aboutsummaryrefslogtreecommitdiff
path: root/tests/managers/thl/test_ledger
diff options
context:
space:
mode:
Diffstat (limited to 'tests/managers/thl/test_ledger')
-rw-r--r--tests/managers/thl/test_ledger/__init__.py0
-rw-r--r--tests/managers/thl/test_ledger/test_lm_accounts.py268
-rw-r--r--tests/managers/thl/test_ledger/test_lm_tx.py235
-rw-r--r--tests/managers/thl/test_ledger/test_lm_tx_entries.py26
-rw-r--r--tests/managers/thl/test_ledger/test_lm_tx_locks.py371
-rw-r--r--tests/managers/thl/test_ledger/test_lm_tx_metadata.py34
-rw-r--r--tests/managers/thl/test_ledger/test_thl_lm_accounts.py411
-rw-r--r--tests/managers/thl/test_ledger/test_thl_lm_bp_payout.py516
-rw-r--r--tests/managers/thl/test_ledger/test_thl_lm_tx.py1762
-rw-r--r--tests/managers/thl/test_ledger/test_thl_lm_tx__user_payouts.py505
-rw-r--r--tests/managers/thl/test_ledger/test_thl_pem.py251
-rw-r--r--tests/managers/thl/test_ledger/test_user_txs.py288
-rw-r--r--tests/managers/thl/test_ledger/test_wallet.py78
13 files changed, 4745 insertions, 0 deletions
diff --git a/tests/managers/thl/test_ledger/__init__.py b/tests/managers/thl/test_ledger/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/managers/thl/test_ledger/__init__.py
diff --git a/tests/managers/thl/test_ledger/test_lm_accounts.py b/tests/managers/thl/test_ledger/test_lm_accounts.py
new file mode 100644
index 0000000..e0d9b0b
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_lm_accounts.py
@@ -0,0 +1,268 @@
+from itertools import product as iproduct
+from random import randint
+from uuid import uuid4
+
+import pytest
+
+from generalresearch.currency import LedgerCurrency
+from generalresearch.managers.base import Permission
+from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerAccountDoesntExistError,
+)
+from generalresearch.managers.thl.ledger_manager.ledger import LedgerManager
+from generalresearch.models.thl.ledger import LedgerAccount, AccountType, Direction
+from generalresearch.models.thl.ledger import (
+ LedgerEntry,
+)
+from test_utils.managers.ledger.conftest import ledger_account
+
+
+@pytest.mark.parametrize(
+ argnames="currency, kind, acct_id",
+ argvalues=list(
+ iproduct(
+ ["USD", "test", "EUR"],
+ ["expense", "wallet", "revenue", "cash"],
+ [uuid4().hex for i in range(3)],
+ )
+ ),
+)
+class TestLedgerAccountManagerNoResults:
+
+ def test_get_account_no_results(self, currency, kind, acct_id, lm):
+ """Try to query for accounts that we know don't exist and confirm that
+ we either get the expected None result or it raises the correct
+ exception
+ """
+ qn = ":".join([currency, kind, acct_id])
+
+ # (1) .get_account is just a wrapper for .get_account_many_ but
+ # call it either way
+ assert lm.get_account(qualified_name=qn, raise_on_error=False) is None
+
+ with pytest.raises(expected_exception=LedgerAccountDoesntExistError):
+ lm.get_account(qualified_name=qn, raise_on_error=True)
+
+ # (2) .get_account_if_exists is another wrapper
+ assert lm.get_account(qualified_name=qn, raise_on_error=False) is None
+
+ def test_get_account_no_results_many(self, currency, kind, acct_id, lm):
+ qn = ":".join([currency, kind, acct_id])
+
+ # (1) .get_many_
+ assert lm.get_account_many_(qualified_names=[qn], raise_on_error=False) == []
+
+ with pytest.raises(expected_exception=LedgerAccountDoesntExistError):
+ lm.get_account_many_(qualified_names=[qn], raise_on_error=True)
+
+ # (2) .get_many
+ assert lm.get_account_many(qualified_names=[qn], raise_on_error=False) == []
+
+ with pytest.raises(expected_exception=LedgerAccountDoesntExistError):
+ lm.get_account_many(qualified_names=[qn], raise_on_error=True)
+
+ # (3) .get_accounts(..)
+ assert lm.get_accounts_if_exists(qualified_names=[qn]) == []
+
+ with pytest.raises(expected_exception=LedgerAccountDoesntExistError):
+ lm.get_accounts(qualified_names=[qn])
+
+
+@pytest.mark.parametrize(
+ argnames="currency, account_type, direction",
+ argvalues=list(
+ iproduct(
+ list(LedgerCurrency),
+ list(AccountType),
+ list(Direction),
+ )
+ ),
+)
+class TestLedgerAccountManagerCreate:
+
+ def test_create_account_error_permission(
+ self, currency, account_type, direction, lm
+ ):
+ """Confirm that the Permission values that are set on the Ledger Manger
+ allow the Creation action to occur.
+ """
+ acct_uuid = uuid4().hex
+
+ account = LedgerAccount(
+ display_name=f"test-{uuid4().hex}",
+ currency=currency,
+ qualified_name=f"{currency.value}:{account_type.value}:{acct_uuid}",
+ account_type=account_type,
+ normal_balance=direction,
+ )
+
+ # (1) With no Permissions defined
+ test_lm = LedgerManager(
+ pg_config=lm.pg_config,
+ permissions=[],
+ redis_config=lm.redis_config,
+ cache_prefix=lm.cache_prefix,
+ testing=lm.testing,
+ )
+
+ with pytest.raises(expected_exception=AssertionError) as excinfo:
+ test_lm.create_account(account=account)
+ assert (
+ str(excinfo.value) == "LedgerManager does not have sufficient permissions"
+ )
+
+ # (2) With Permissions defined, but not CREATE
+ test_lm = LedgerManager(
+ pg_config=lm.pg_config,
+ permissions=[Permission.READ, Permission.UPDATE, Permission.DELETE],
+ redis_config=lm.redis_config,
+ cache_prefix=lm.cache_prefix,
+ testing=lm.testing,
+ )
+
+ with pytest.raises(expected_exception=AssertionError) as excinfo:
+ test_lm.create_account(account=account)
+ assert (
+ str(excinfo.value) == "LedgerManager does not have sufficient permissions"
+ )
+
+ def test_create(self, currency, account_type, direction, lm):
+ """Confirm that the Permission values that are set on the Ledger Manger
+ allow the Creation action to occur.
+ """
+
+ acct_uuid = uuid4().hex
+ qn = f"{currency.value}:{account_type.value}:{acct_uuid}"
+
+ acct_model = LedgerAccount(
+ uuid=acct_uuid,
+ display_name=f"test-{uuid4().hex}",
+ currency=currency,
+ qualified_name=qn,
+ account_type=account_type,
+ normal_balance=direction,
+ )
+ account = lm.create_account(account=acct_model)
+ assert isinstance(account, LedgerAccount)
+
+ # Query for, and make sure the Account was saved in the DB
+ res = lm.get_account(qualified_name=qn, raise_on_error=True)
+ assert account.uuid == res.uuid
+
+ def test_get_or_create(self, currency, account_type, direction, lm):
+ """Confirm that the Permission values that are set on the Ledger Manger
+ allow the Creation action to occur.
+ """
+
+ acct_uuid = uuid4().hex
+ qn = f"{currency.value}:{account_type.value}:{acct_uuid}"
+
+ acct_model = LedgerAccount(
+ uuid=acct_uuid,
+ display_name=f"test-{uuid4().hex}",
+ currency=currency,
+ qualified_name=qn,
+ account_type=account_type,
+ normal_balance=direction,
+ )
+ account = lm.get_account_or_create(account=acct_model)
+ assert isinstance(account, LedgerAccount)
+
+ # Query for, and make sure the Account was saved in the DB
+ res = lm.get_account(qualified_name=qn, raise_on_error=True)
+ assert account.uuid == res.uuid
+
+
+class TestLedgerAccountManagerGet:
+
+ def test_get(self, ledger_account, lm):
+ res = lm.get_account(qualified_name=ledger_account.qualified_name)
+ assert res.uuid == ledger_account.uuid
+
+ res = lm.get_account_many(qualified_names=[ledger_account.qualified_name])
+ assert len(res) == 1
+ assert res[0].uuid == ledger_account.uuid
+
+ res = lm.get_accounts(qualified_names=[ledger_account.qualified_name])
+ assert len(res) == 1
+ assert res[0].uuid == ledger_account.uuid
+
+ # TODO: I can't test the get_balance without first having Transaction
+ # creation working
+
+ def test_get_balance_empty(
+ self, ledger_account, ledger_account_credit, ledger_account_debit, ledger_tx, lm
+ ):
+ res = lm.get_account_balance(account=ledger_account)
+ assert res == 0
+
+ res = lm.get_account_balance(account=ledger_account_credit)
+ assert res == 100
+
+ res = lm.get_account_balance(account=ledger_account_debit)
+ assert res == 100
+
+ @pytest.mark.parametrize("n_times", range(5))
+ def test_get_account_filtered_balance(
+ self,
+ ledger_account,
+ ledger_account_credit,
+ ledger_account_debit,
+ ledger_tx,
+ n_times,
+ lm,
+ ):
+ """Try searching for random metadata and confirm it's always 0 because
+ Tx can be found.
+ """
+ rand_key = f"key-{uuid4().hex[:10]}"
+ rand_value = uuid4().hex
+
+ assert (
+ lm.get_account_filtered_balance(
+ account=ledger_account, metadata_key=rand_key, metadata_value=rand_value
+ )
+ == 0
+ )
+
+ # Let's create a transaction with this metadata to confirm it saves
+ # and that we can filter it back
+ rand_amount = randint(10, 1_000)
+
+ lm.create_tx(
+ entries=[
+ LedgerEntry(
+ direction=Direction.CREDIT,
+ account_uuid=ledger_account_credit.uuid,
+ amount=rand_amount,
+ ),
+ LedgerEntry(
+ direction=Direction.DEBIT,
+ account_uuid=ledger_account_debit.uuid,
+ amount=rand_amount,
+ ),
+ ],
+ metadata={rand_key: rand_value},
+ )
+
+ assert (
+ lm.get_account_filtered_balance(
+ account=ledger_account_credit,
+ metadata_key=rand_key,
+ metadata_value=rand_value,
+ )
+ == rand_amount
+ )
+
+ assert (
+ lm.get_account_filtered_balance(
+ account=ledger_account_debit,
+ metadata_key=rand_key,
+ metadata_value=rand_value,
+ )
+ == rand_amount
+ )
+
+ def test_get_balance_timerange_empty(self, ledger_account, lm):
+ res = lm.get_account_balance_timerange(account=ledger_account)
+ assert res == 0
diff --git a/tests/managers/thl/test_ledger/test_lm_tx.py b/tests/managers/thl/test_ledger/test_lm_tx.py
new file mode 100644
index 0000000..37b7ba3
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_lm_tx.py
@@ -0,0 +1,235 @@
+from decimal import Decimal
+from random import randint
+from uuid import uuid4
+
+import pytest
+
+from generalresearch.currency import LedgerCurrency
+from generalresearch.managers.thl.ledger_manager.ledger import LedgerManager
+from generalresearch.models.thl.ledger import (
+ Direction,
+ LedgerEntry,
+ LedgerTransaction,
+)
+
+
+class TestLedgerManagerCreateTx:
+
+ def test_create_account_error_permission(self, lm):
+ """Confirm that the Permission values that are set on the Ledger Manger
+ allow the Creation action to occur.
+ """
+ acct_uuid = uuid4().hex
+
+ # (1) With no Permissions defined
+ test_lm = LedgerManager(
+ pg_config=lm.pg_config,
+ permissions=[],
+ redis_config=lm.redis_config,
+ cache_prefix=lm.cache_prefix,
+ testing=lm.testing,
+ )
+
+ with pytest.raises(expected_exception=AssertionError) as excinfo:
+ test_lm.create_tx(entries=[])
+ assert (
+ str(excinfo.value)
+ == "LedgerTransactionManager has insufficient Permissions"
+ )
+
+ def test_create_assertions(self, ledger_account_debit, ledger_account_credit, lm):
+ with pytest.raises(expected_exception=ValueError) as excinfo:
+ lm.create_tx(
+ entries=[
+ {
+ "direction": Direction.CREDIT,
+ "account_uuid": uuid4().hex,
+ "amount": randint(a=1, b=100),
+ }
+ ]
+ )
+ assert (
+ "Assertion failed, ledger transaction must have 2 or more entries"
+ in str(excinfo.value)
+ )
+
+ def test_create(self, ledger_account_credit, ledger_account_debit, lm):
+ amount = int(Decimal("1.00") * 100)
+
+ entries = [
+ LedgerEntry(
+ direction=Direction.CREDIT,
+ account_uuid=ledger_account_credit.uuid,
+ amount=amount,
+ ),
+ LedgerEntry(
+ direction=Direction.DEBIT,
+ account_uuid=ledger_account_debit.uuid,
+ amount=amount,
+ ),
+ ]
+
+ # Create a Transaction and validate the operation was successful
+ tx = lm.create_tx(entries=entries)
+ assert isinstance(tx, LedgerTransaction)
+
+ res = lm.get_tx_by_id(transaction_id=tx.id)
+ assert isinstance(res, LedgerTransaction)
+ assert len(res.entries) == 2
+ assert tx.id == res.id
+
+ def test_create_and_reverse(self, ledger_account_credit, ledger_account_debit, lm):
+ amount = int(Decimal("1.00") * 100)
+
+ entries = [
+ LedgerEntry(
+ direction=Direction.CREDIT,
+ account_uuid=ledger_account_credit.uuid,
+ amount=amount,
+ ),
+ LedgerEntry(
+ direction=Direction.DEBIT,
+ account_uuid=ledger_account_debit.uuid,
+ amount=amount,
+ ),
+ ]
+
+ tx = lm.create_tx(entries=entries)
+ res = lm.get_tx_by_id(transaction_id=tx.id)
+ assert res.id == tx.id
+
+ assert lm.get_account_balance(account=ledger_account_credit) == 100
+ assert lm.get_account_balance(account=ledger_account_debit) == 100
+ assert lm.check_ledger_balanced() is True
+
+ # Reverse it
+ entries = [
+ LedgerEntry(
+ direction=Direction.DEBIT,
+ account_uuid=ledger_account_credit.uuid,
+ amount=amount,
+ ),
+ LedgerEntry(
+ direction=Direction.CREDIT,
+ account_uuid=ledger_account_debit.uuid,
+ amount=amount,
+ ),
+ ]
+
+ tx = lm.create_tx(entries=entries)
+ res = lm.get_tx_by_id(transaction_id=tx.id)
+ assert res.id == tx.id
+
+ assert lm.get_account_balance(ledger_account_credit) == 0
+ assert lm.get_account_balance(ledger_account_debit) == 0
+ assert lm.check_ledger_balanced()
+
+ # subtract again
+ entries = [
+ LedgerEntry(
+ direction=Direction.DEBIT,
+ account_uuid=ledger_account_credit.uuid,
+ amount=amount,
+ ),
+ LedgerEntry(
+ direction=Direction.CREDIT,
+ account_uuid=ledger_account_debit.uuid,
+ amount=amount,
+ ),
+ ]
+ tx = lm.create_tx(entries=entries)
+ res = lm.get_tx_by_id(transaction_id=tx.id)
+ assert res.id == tx.id
+
+ assert lm.get_account_balance(ledger_account_credit) == -100
+ assert lm.get_account_balance(ledger_account_debit) == -100
+ assert lm.check_ledger_balanced()
+
+
+class TestLedgerManagerGetTx:
+
+ # @pytest.mark.parametrize("currency", [LedgerCurrency.TEST], indirect=True)
+ def test_get_tx_by_id(self, ledger_tx, lm):
+ with pytest.raises(expected_exception=AssertionError):
+ lm.get_tx_by_id(transaction_id=ledger_tx)
+
+ res = lm.get_tx_by_id(transaction_id=ledger_tx.id)
+ assert res.id == ledger_tx.id
+
+ # @pytest.mark.parametrize("currency", [LedgerCurrency.TEST], indirect=True)
+ def test_get_tx_by_ids(self, ledger_tx, lm):
+ res = lm.get_tx_by_id(transaction_id=ledger_tx.id)
+ assert res.id == ledger_tx.id
+
+ @pytest.mark.parametrize(
+ "tag", [f"{LedgerCurrency.TEST}:{uuid4().hex}"], indirect=True
+ )
+ def test_get_tx_ids_by_tag(self, ledger_tx, tag, lm):
+ # (1) search for a random tag
+ res = lm.get_tx_ids_by_tag(tag="aaa:bbb")
+ assert isinstance(res, set)
+ assert len(res) == 0
+
+ # (2) search for the tag that was used during ledger_transaction creation
+ res = lm.get_tx_ids_by_tag(tag=tag)
+ assert isinstance(res, set)
+ assert len(res) == 1
+
+ def test_get_tx_by_tag(self, ledger_tx, tag, lm):
+ # (1) search for a random tag
+ res = lm.get_tx_by_tag(tag="aaa:bbb")
+ assert isinstance(res, list)
+ assert len(res) == 0
+
+ # (2) search for the tag that was used during ledger_transaction creation
+ res = lm.get_tx_by_tag(tag=tag)
+ assert isinstance(res, list)
+ assert len(res) == 1
+
+ assert isinstance(res[0], LedgerTransaction)
+ assert ledger_tx.id == res[0].id
+
+ def test_get_tx_filtered_by_account(
+ self, ledger_tx, ledger_account, ledger_account_debit, ledger_account_credit, lm
+ ):
+ # (1) Do basic assertion checks first
+ with pytest.raises(expected_exception=AssertionError) as excinfo:
+ lm.get_tx_filtered_by_account(account_uuid=ledger_account)
+ assert str(excinfo.value) == "account_uuid must be a str"
+
+ # (2) This search doesn't return anything because this ledger account
+ # wasn't actually used in the entries for the ledger_transaction
+ res = lm.get_tx_filtered_by_account(account_uuid=ledger_account.uuid)
+ assert len(res) == 0
+
+ # (3) Either the credit or the debit example ledger_accounts wll work
+ # to find this transaction because they're both used in the entries
+ res = lm.get_tx_filtered_by_account(account_uuid=ledger_account_debit.uuid)
+ assert len(res) == 1
+ assert res[0].id == ledger_tx.id
+
+ res = lm.get_tx_filtered_by_account(account_uuid=ledger_account_credit.uuid)
+ assert len(res) == 1
+ assert ledger_tx.id == res[0].id
+
+ res2 = lm.get_tx_by_id(transaction_id=ledger_tx.id)
+ assert res2.model_dump_json() == res[0].model_dump_json()
+
+ def test_filter_metadata(self, ledger_tx, tx_metadata, lm):
+ key, value = next(iter(tx_metadata.items()))
+
+ # (1) Confirm a random key,value pair returns nothing
+ res = lm.get_tx_filtered_by_metadata(
+ metadata_key=f"key-{uuid4().hex[:10]}", metadata_value=uuid4().hex[:12]
+ )
+ assert len(res) == 0
+
+ # (2) confirm a key,value pair return the correct results
+ res = lm.get_tx_filtered_by_metadata(metadata_key=key, metadata_value=value)
+ assert len(res) == 1
+
+ # assert 0 == THL_lm.get_filtered_account_balance(account2, "thl_wall", "ccc")
+ # assert 300 == THL_lm.get_filtered_account_balance(account1, "thl_wall", "aaa")
+ # assert 300 == THL_lm.get_filtered_account_balance(account2, "thl_wall", "aaa")
+ # assert 0 == THL_lm.get_filtered_account_balance(account3, "thl_wall", "ccc")
+ # assert THL_lm.check_ledger_balanced()
diff --git a/tests/managers/thl/test_ledger/test_lm_tx_entries.py b/tests/managers/thl/test_ledger/test_lm_tx_entries.py
new file mode 100644
index 0000000..5bf1c48
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_lm_tx_entries.py
@@ -0,0 +1,26 @@
+from generalresearch.models.thl.ledger import LedgerEntry
+
+
+class TestLedgerEntryManager:
+
+ def test_get_tx_entries_by_tx(self, ledger_tx, lm):
+ # First confirm the Ledger TX exists with 2 Entries
+ res = lm.get_tx_by_id(transaction_id=ledger_tx.id)
+ assert len(res.entries) == 2
+
+ tx_entries = lm.get_tx_entries_by_tx(transaction=ledger_tx)
+ assert len(tx_entries) == 2
+
+ assert res.entries == tx_entries
+ assert isinstance(tx_entries[0], LedgerEntry)
+
+ def test_get_tx_entries_by_txs(self, ledger_tx, lm):
+ # First confirm the Ledger TX exists with 2 Entries
+ res = lm.get_tx_by_id(transaction_id=ledger_tx.id)
+ assert len(res.entries) == 2
+
+ tx_entries = lm.get_tx_entries_by_txs(transactions=[ledger_tx])
+ assert len(tx_entries) == 2
+
+ assert res.entries == tx_entries
+ assert isinstance(tx_entries[0], LedgerEntry)
diff --git a/tests/managers/thl/test_ledger/test_lm_tx_locks.py b/tests/managers/thl/test_ledger/test_lm_tx_locks.py
new file mode 100644
index 0000000..df2611b
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_lm_tx_locks.py
@@ -0,0 +1,371 @@
+import logging
+from datetime import datetime, timezone, timedelta
+from decimal import Decimal
+from typing import Callable
+
+import pytest
+
+from generalresearch.managers.thl.ledger_manager.conditions import (
+ generate_condition_mp_payment,
+)
+from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerTransactionCreateLockError,
+ LedgerTransactionFlagAlreadyExistsError,
+ LedgerTransactionCreateError,
+)
+from generalresearch.models import Source
+from generalresearch.models.thl.ledger import LedgerTransaction
+from generalresearch.models.thl.session import (
+ Wall,
+ Status,
+ StatusCode1,
+ Session,
+ WallAdjustedStatus,
+)
+from generalresearch.models.thl.user import User
+from test_utils.models.conftest import user_factory, session, product_user_wallet_no
+
+logger = logging.getLogger("LedgerManager")
+
+
+class TestLedgerLocks:
+
+ def test_a(
+ self,
+ user_factory,
+ session_factory,
+ product_user_wallet_no,
+ create_main_accounts,
+ caplog,
+ thl_lm,
+ lm,
+ utc_hour_ago,
+ currency,
+ wall_factory,
+ delete_ledger_db,
+ ):
+ """
+ TODO: This whole test is confusing a I don't really understand.
+ It needs to be better documented and explained what we want
+ it to do and evaluate...
+ """
+ delete_ledger_db()
+ create_main_accounts()
+
+ user: User = user_factory(product=product_user_wallet_no)
+ s1 = session_factory(
+ user=user,
+ wall_count=3,
+ wall_req_cpis=[Decimal("1.23"), Decimal("3.21"), Decimal("4")],
+ wall_statuses=[Status.COMPLETE, Status.COMPLETE, Status.COMPLETE],
+ )
+
+ # A User does a Wall Completion in Session=1
+ w1 = s1.wall_events[0]
+ tx = thl_lm.create_tx_task_complete(wall=w1, user=user, created=w1.started)
+ assert isinstance(tx, LedgerTransaction)
+
+ # A User does another Wall Completion in Session=1
+ w2 = s1.wall_events[1]
+ tx = thl_lm.create_tx_task_complete(wall=w2, user=user, created=w2.started)
+ assert isinstance(tx, LedgerTransaction)
+
+ # That first Wall Complete was "adjusted" to instead be marked
+ # as a Failure
+ w1.update(
+ adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL,
+ adjusted_cpi=0,
+ adjusted_timestamp=utc_hour_ago + timedelta(hours=1),
+ )
+ tx = thl_lm.create_tx_task_adjustment(wall=w1, user=user)
+ assert isinstance(tx, LedgerTransaction)
+
+ # A User does another! Wall Completion in Session=1; however, we
+ # don't create a transaction for it
+ w3 = s1.wall_events[2]
+
+ # Make sure we clear any flags/locks first
+ lock_key = f"{currency.value}:thl_wall:{w3.uuid}"
+ lock_name = f"{lm.cache_prefix}:transaction_lock:{lock_key}"
+ flag_name = f"{lm.cache_prefix}:transaction_flag:{lock_key}"
+ lm.redis_client.delete(lock_name)
+ lm.redis_client.delete(flag_name)
+
+ # Despite the
+ f1 = generate_condition_mp_payment(wall=w1)
+ f2 = generate_condition_mp_payment(wall=w2)
+ f3 = generate_condition_mp_payment(wall=w3)
+ assert f1(lm=lm) is False
+ assert f2(lm=lm) is False
+ assert f3(lm=lm) is True
+
+ condition = f3
+ create_tx_func = lambda: thl_lm.create_tx_task_complete_(wall=w3, user=user)
+ assert isinstance(create_tx_func, Callable)
+ assert f3(lm) is True
+
+ lm.redis_client.delete(flag_name)
+ lm.redis_client.delete(lock_name)
+
+ tx = thl_lm.create_tx_protected(
+ lock_key=lock_key, condition=condition, create_tx_func=create_tx_func
+ )
+ assert f3(lm) is False
+
+ # purposely hold the lock open
+ tx = None
+ lm.redis_client.set(lock_name, "1")
+ with caplog.at_level(logging.ERROR):
+ with pytest.raises(expected_exception=LedgerTransactionCreateLockError):
+ tx = thl_lm.create_tx_protected(
+ lock_key=lock_key,
+ condition=condition,
+ create_tx_func=create_tx_func,
+ )
+ assert tx is None
+ assert "Unable to acquire lock within the time specified" in caplog.text
+ lm.redis_client.delete(lock_name)
+
+ def test_locking(
+ self,
+ user_factory,
+ product_user_wallet_no,
+ create_main_accounts,
+ delete_ledger_db,
+ caplog,
+ thl_lm,
+ lm,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ now = datetime.now(timezone.utc) - timedelta(hours=1)
+ user: User = user_factory(product=product_user_wallet_no)
+
+ # A User does a Wall complete on Session.id=1 and the transaction is
+ # logged to the ledger
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.DYNATA,
+ req_survey_id="xxx",
+ req_cpi=Decimal("1.23"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=now,
+ finished=now + timedelta(seconds=1),
+ )
+ thl_lm.create_tx_task_complete(wall=wall1, user=user, created=wall1.started)
+
+ # A User does a Wall complete on Session.id=1 and the transaction is
+ # logged to the ledger
+ wall2 = Wall(
+ user_id=user.user_id,
+ source=Source.FULL_CIRCLE,
+ req_survey_id="yyy",
+ req_cpi=Decimal("3.21"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=now,
+ finished=now + timedelta(seconds=1),
+ )
+ thl_lm.create_tx_task_complete(wall=wall2, user=user, created=wall2.started)
+
+ # An hour later, the first wall complete is adjusted to a Failure and
+ # it's tracked in the ledger
+ wall1.update(
+ adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL,
+ adjusted_cpi=0,
+ adjusted_timestamp=now + timedelta(hours=1),
+ )
+ thl_lm.create_tx_task_adjustment(wall=wall1, user=user)
+
+ # A User does a Wall complete on Session.id=1 and the transaction
+ # IS NOT logged to the ledger
+ wall3 = Wall(
+ user_id=user.user_id,
+ source=Source.DYNATA,
+ req_survey_id="xxx",
+ req_cpi=Decimal("4"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=now,
+ finished=now + timedelta(seconds=1),
+ uuid="867a282d8b4d40d2a2093d75b802b629",
+ )
+
+ revenue_account = thl_lm.get_account_task_complete_revenue()
+ assert 0 == thl_lm.get_account_filtered_balance(
+ account=revenue_account,
+ metadata_key="thl_wall",
+ metadata_value=wall3.uuid,
+ )
+ # Make sure we clear any flags/locks first
+ lock_key = f"test:thl_wall:{wall3.uuid}"
+ lock_name = f"{lm.cache_prefix}:transaction_lock:{lock_key}"
+ flag_name = f"{lm.cache_prefix}:transaction_flag:{lock_key}"
+ lm.redis_client.delete(lock_name)
+ lm.redis_client.delete(flag_name)
+
+ # Purposely hold the lock open
+ lm.redis_client.set(name=lock_name, value="1")
+ with caplog.at_level(logging.DEBUG):
+ with pytest.raises(expected_exception=LedgerTransactionCreateLockError):
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall3, user=user, created=wall3.started
+ )
+ assert isinstance(tx, LedgerTransaction)
+ assert "Unable to acquire lock within the time specified" in caplog.text
+
+ # Release the lock
+ lm.redis_client.delete(lock_name)
+
+ # Set the redis flag to indicate it has been run
+ lm.redis_client.set(flag_name, "1")
+ # with self.assertLogs(logger=logger, level=logging.DEBUG) as cm2:
+ with pytest.raises(expected_exception=LedgerTransactionFlagAlreadyExistsError):
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall3, user=user, created=wall3.started
+ )
+ # self.assertIn("entered_lock: True, flag_set: True", cm2.output[0])
+
+ # Unset the flag
+ lm.redis_client.delete(flag_name)
+
+ assert 0 == lm.get_account_filtered_balance(
+ account=revenue_account,
+ metadata_key="thl_wall",
+ metadata_value=wall3.uuid,
+ )
+
+ # Now actually run it
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall3, user=user, created=wall3.started
+ )
+ assert tx is not None
+
+ # Run it again, should return None
+ # Confirm the Exception inheritance works
+ tx = None
+ with pytest.raises(expected_exception=LedgerTransactionCreateError):
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall3, user=user, created=wall3.started
+ )
+ assert tx is None
+
+ # clear the redis flag, it should query the db
+ assert lm.redis_client.get(flag_name) is not None
+ lm.redis_client.delete(flag_name)
+ assert lm.redis_client.get(flag_name) is None
+
+ with pytest.raises(expected_exception=LedgerTransactionCreateError):
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall3, user=user, created=wall3.started
+ )
+
+ assert 400 == thl_lm.get_account_filtered_balance(
+ account=revenue_account,
+ metadata_key="thl_wall",
+ metadata_value=wall3.uuid,
+ )
+
+ def test_bp_payment_without_locks(
+ self, user_factory, product_user_wallet_no, create_main_accounts, thl_lm, lm
+ ):
+ user: User = user_factory(product=product_user_wallet_no)
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.SAGO,
+ req_survey_id="xxx",
+ req_cpi=Decimal("0.50"),
+ session_id=3,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=datetime.now(timezone.utc),
+ finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ )
+
+ thl_lm.create_tx_task_complete(wall=wall1, user=user, created=wall1.started)
+ session = Session(started=wall1.started, user=user, wall_events=[wall1])
+ status, status_code_1 = session.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = session.determine_payments()
+ session.update(
+ **{
+ "status": status,
+ "status_code_1": status_code_1,
+ "finished": session.started + timedelta(minutes=10),
+ "payout": bp_pay,
+ "user_payout": user_pay,
+ }
+ )
+ print(thl_net, commission_amount, bp_pay, user_pay)
+
+ # Run it 3 times without any checks, and it gets made three times!
+ thl_lm.create_tx_bp_payment(session=session, created=wall1.started)
+ thl_lm.create_tx_bp_payment_(session=session, created=wall1.started)
+ thl_lm.create_tx_bp_payment_(session=session, created=wall1.started)
+
+ bp_wallet = thl_lm.get_account_or_create_bp_wallet(product=user.product)
+ assert 48 * 3 == lm.get_account_balance(account=bp_wallet)
+ assert 48 * 3 == thl_lm.get_account_filtered_balance(
+ account=bp_wallet, metadata_key="thl_session", metadata_value=session.uuid
+ )
+ assert lm.check_ledger_balanced()
+
+ def test_bp_payment_with_locks(
+ self, user_factory, product_user_wallet_no, create_main_accounts, thl_lm, lm
+ ):
+ user: User = user_factory(product=product_user_wallet_no)
+
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.SAGO,
+ req_survey_id="xxx",
+ req_cpi=Decimal("0.50"),
+ session_id=3,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=datetime.now(timezone.utc),
+ finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ )
+
+ thl_lm.create_tx_task_complete(wall1, user, created=wall1.started)
+ session = Session(started=wall1.started, user=user, wall_events=[wall1])
+ status, status_code_1 = session.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = session.determine_payments()
+ session.update(
+ **{
+ "status": status,
+ "status_code_1": status_code_1,
+ "finished": session.started + timedelta(minutes=10),
+ "payout": bp_pay,
+ "user_payout": user_pay,
+ }
+ )
+ print(thl_net, commission_amount, bp_pay, user_pay)
+
+ # Make sure we clear any flags/locks first
+ lock_key = f"test:thl_wall:{wall1.uuid}"
+ lock_name = f"{lm.cache_prefix}:transaction_lock:{lock_key}"
+ flag_name = f"{lm.cache_prefix}:transaction_flag:{lock_key}"
+ lm.redis_client.delete(lock_name)
+ lm.redis_client.delete(flag_name)
+
+ # Run it 3 times with check, and it gets made once!
+ thl_lm.create_tx_bp_payment(session=session, created=wall1.started)
+ with pytest.raises(expected_exception=LedgerTransactionCreateError):
+ thl_lm.create_tx_bp_payment(session=session, created=wall1.started)
+
+ with pytest.raises(expected_exception=LedgerTransactionCreateError):
+ thl_lm.create_tx_bp_payment(session=session, created=wall1.started)
+
+ bp_wallet = thl_lm.get_account_or_create_bp_wallet(product=user.product)
+ assert 48 == thl_lm.get_account_balance(bp_wallet)
+ assert 48 == thl_lm.get_account_filtered_balance(
+ account=bp_wallet,
+ metadata_key="thl_session",
+ metadata_value=session.uuid,
+ )
+ assert lm.check_ledger_balanced()
diff --git a/tests/managers/thl/test_ledger/test_lm_tx_metadata.py b/tests/managers/thl/test_ledger/test_lm_tx_metadata.py
new file mode 100644
index 0000000..5d12633
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_lm_tx_metadata.py
@@ -0,0 +1,34 @@
+class TestLedgerMetadataManager:
+
+ def test_get_tx_metadata_by_txs(self, ledger_tx, lm):
+ # First confirm the Ledger TX exists with 2 Entries
+ res = lm.get_tx_by_id(transaction_id=ledger_tx.id)
+ assert isinstance(res.metadata, dict)
+
+ tx_metadatas = lm.get_tx_metadata_by_txs(transactions=[ledger_tx])
+ assert isinstance(tx_metadatas, dict)
+ assert isinstance(tx_metadatas[ledger_tx.id], dict)
+
+ assert res.metadata == tx_metadatas[ledger_tx.id]
+
+ def test_get_tx_metadata_ids_by_tx(self, ledger_tx, lm):
+ # First confirm the Ledger TX exists with 2 Entries
+ res = lm.get_tx_by_id(transaction_id=ledger_tx.id)
+ tx_metadata_cnt = len(res.metadata.keys())
+
+ tx_metadata_ids = lm.get_tx_metadata_ids_by_tx(transaction=ledger_tx)
+ assert isinstance(tx_metadata_ids, set)
+ assert isinstance(list(tx_metadata_ids)[0], int)
+
+ assert tx_metadata_cnt == len(tx_metadata_ids)
+
+ def test_get_tx_metadata_ids_by_txs(self, ledger_tx, lm):
+ # First confirm the Ledger TX exists with 2 Entries
+ res = lm.get_tx_by_id(transaction_id=ledger_tx.id)
+ tx_metadata_cnt = len(res.metadata.keys())
+
+ tx_metadata_ids = lm.get_tx_metadata_ids_by_txs(transactions=[ledger_tx])
+ assert isinstance(tx_metadata_ids, set)
+ assert isinstance(list(tx_metadata_ids)[0], int)
+
+ assert tx_metadata_cnt == len(tx_metadata_ids)
diff --git a/tests/managers/thl/test_ledger/test_thl_lm_accounts.py b/tests/managers/thl/test_ledger/test_thl_lm_accounts.py
new file mode 100644
index 0000000..01d5fe1
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_thl_lm_accounts.py
@@ -0,0 +1,411 @@
+from uuid import uuid4
+
+import pytest
+
+
+class TestThlLedgerManagerAccounts:
+
+ def test_get_account_or_create_user_wallet(self, user, thl_lm, lm):
+ from generalresearch.currency import LedgerCurrency
+ from generalresearch.models.thl.ledger import (
+ LedgerAccount,
+ Direction,
+ AccountType,
+ )
+
+ account = thl_lm.get_account_or_create_user_wallet(user=user)
+ assert isinstance(account, LedgerAccount)
+
+ assert user.uuid in account.qualified_name
+ assert account.display_name == f"User Wallet {user.uuid}"
+ assert account.account_type == AccountType.USER_WALLET
+ assert account.normal_balance == Direction.CREDIT
+ assert account.reference_type == "user"
+ assert account.reference_uuid == user.uuid
+ assert account.currency == LedgerCurrency.TEST
+
+ # Actually query for it to confirm
+ res = lm.get_account(qualified_name=account.qualified_name, raise_on_error=True)
+ assert res.model_dump_json() == account.model_dump_json()
+
+ def test_get_account_or_create_bp_wallet(self, product, thl_lm, lm):
+ from generalresearch.currency import LedgerCurrency
+ from generalresearch.models.thl.ledger import (
+ LedgerAccount,
+ Direction,
+ AccountType,
+ )
+
+ account = thl_lm.get_account_or_create_bp_wallet(product=product)
+ assert isinstance(account, LedgerAccount)
+
+ assert product.uuid in account.qualified_name
+ assert account.display_name == f"BP Wallet {product.uuid}"
+ assert account.account_type == AccountType.BP_WALLET
+ assert account.normal_balance == Direction.CREDIT
+ assert account.reference_type == "bp"
+ assert account.reference_uuid == product.uuid
+ assert account.currency == LedgerCurrency.TEST
+
+ # Actually query for it to confirm
+ res = lm.get_account(qualified_name=account.qualified_name, raise_on_error=True)
+ assert res.model_dump_json() == account.model_dump_json()
+
+ def test_get_account_or_create_bp_commission(self, product, thl_lm, lm):
+ from generalresearch.currency import LedgerCurrency
+ from generalresearch.models.thl.ledger import (
+ Direction,
+ AccountType,
+ )
+
+ account = thl_lm.get_account_or_create_bp_commission(product=product)
+
+ assert product.uuid in account.qualified_name
+ assert account.display_name == f"Revenue from commission {product.uuid}"
+ assert account.account_type == AccountType.REVENUE
+ assert account.normal_balance == Direction.CREDIT
+ assert account.reference_type == "bp"
+ assert account.reference_uuid == product.uuid
+ assert account.currency == LedgerCurrency.TEST
+
+ # Actually query for it to confirm
+ res = lm.get_account(qualified_name=account.qualified_name, raise_on_error=True)
+ assert res.model_dump_json() == account.model_dump_json()
+
+ @pytest.mark.parametrize("expense", ["tango", "paypal", "gift", "tremendous"])
+ def test_get_account_or_create_bp_expense(self, product, expense, thl_lm, lm):
+ from generalresearch.currency import LedgerCurrency
+ from generalresearch.models.thl.ledger import (
+ Direction,
+ AccountType,
+ )
+
+ account = thl_lm.get_account_or_create_bp_expense(
+ product=product, expense_name=expense
+ )
+ assert product.uuid in account.qualified_name
+ assert account.display_name == f"Expense {expense} {product.uuid}"
+ assert account.account_type == AccountType.EXPENSE
+ assert account.normal_balance == Direction.DEBIT
+ assert account.reference_type == "bp"
+ assert account.reference_uuid == product.uuid
+ assert account.currency == LedgerCurrency.TEST
+
+ # Actually query for it to confirm
+ res = lm.get_account(qualified_name=account.qualified_name, raise_on_error=True)
+ assert res.model_dump_json() == account.model_dump_json()
+
+ def test_get_or_create_bp_pending_payout_account(self, product, thl_lm, lm):
+ from generalresearch.currency import LedgerCurrency
+ from generalresearch.models.thl.ledger import (
+ Direction,
+ AccountType,
+ )
+
+ account = thl_lm.get_or_create_bp_pending_payout_account(product=product)
+
+ assert product.uuid in account.qualified_name
+ assert account.display_name == f"BP Wallet Pending {product.uuid}"
+ assert account.account_type == AccountType.BP_WALLET
+ assert account.normal_balance == Direction.CREDIT
+ assert account.reference_type == "bp"
+ assert account.reference_uuid == product.uuid
+ assert account.currency == LedgerCurrency.TEST
+
+ # Actually query for it to confirm
+ res = lm.get_account(qualified_name=account.qualified_name, raise_on_error=True)
+ assert res.model_dump_json() == account.model_dump_json()
+
+ def test_get_account_task_complete_revenue_raises(
+ self, delete_ledger_db, thl_lm, lm
+ ):
+ from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerAccountDoesntExistError,
+ )
+
+ delete_ledger_db()
+
+ with pytest.raises(expected_exception=LedgerAccountDoesntExistError):
+ thl_lm.get_account_task_complete_revenue()
+
+ def test_get_account_task_complete_revenue(
+ self, account_cash, account_revenue_task_complete, thl_lm, lm
+ ):
+ from generalresearch.models.thl.ledger import (
+ LedgerAccount,
+ AccountType,
+ )
+
+ res = thl_lm.get_account_task_complete_revenue()
+ assert isinstance(res, LedgerAccount)
+ assert res.reference_type is None
+ assert res.reference_uuid is None
+ assert res.account_type == AccountType.REVENUE
+ assert res.display_name == "Cash flow task complete"
+
+ def test_get_account_cash_raises(self, delete_ledger_db, thl_lm, lm):
+ from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerAccountDoesntExistError,
+ )
+
+ delete_ledger_db()
+
+ with pytest.raises(expected_exception=LedgerAccountDoesntExistError):
+ thl_lm.get_account_cash()
+
+ def test_get_account_cash(self, account_cash, thl_lm, lm):
+ from generalresearch.models.thl.ledger import (
+ LedgerAccount,
+ AccountType,
+ )
+
+ res = thl_lm.get_account_cash()
+ assert isinstance(res, LedgerAccount)
+ assert res.reference_type is None
+ assert res.reference_uuid is None
+ assert res.account_type == AccountType.CASH
+ assert res.display_name == "Operating Cash Account"
+
+ def test_get_accounts(self, setup_accounts, product, user_factory, thl_lm, lm, lam):
+ from generalresearch.models.thl.user import User
+ from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerAccountDoesntExistError,
+ )
+
+ user1: User = user_factory(product=product)
+ user2: User = user_factory(product=product)
+
+ account1 = thl_lm.get_account_or_create_bp_wallet(product=product)
+
+ # (1) known account and confirm it comes back
+ res = lm.get_account(qualified_name=account1.qualified_name)
+ assert account1.model_dump_json() == res.model_dump_json()
+
+ # (2) known accounts and confirm they both come back
+ res = lam.get_accounts(qualified_names=[account1.qualified_name])
+ assert isinstance(res, list)
+ assert len(res) == 1
+ assert account1 in res
+
+ # Get 2 known and 1 made up qualified names, and confirm it raises
+ # an error
+ with pytest.raises(LedgerAccountDoesntExistError):
+ lam.get_accounts(
+ qualified_names=[
+ account1.qualified_name,
+ f"test:bp_wall:{uuid4().hex}",
+ ]
+ )
+
+ def test_get_accounts_if_exists(self, product_factory, currency, thl_lm, lm):
+ from generalresearch.models.thl.product import Product
+
+ p1: Product = product_factory()
+ p2: Product = product_factory()
+
+ account1 = thl_lm.get_account_or_create_bp_wallet(product=p1)
+ account2 = thl_lm.get_account_or_create_bp_wallet(product=p2)
+
+ # (1) known account and confirm it comes back
+ res = lm.get_account(qualified_name=account1.qualified_name)
+ assert account1.model_dump_json() == res.model_dump_json()
+
+ # (2) known accounts and confirm they both come back
+ res = lm.get_accounts(
+ qualified_names=[account1.qualified_name, account2.qualified_name]
+ )
+ assert isinstance(res, list)
+ assert len(res) == 2
+ assert account1 in res
+ assert account2 in res
+
+ # Get 2 known and 1 made up qualified names, and confirm only 2
+ # come back
+ lm.get_accounts_if_exists(
+ qualified_names=[
+ account1.qualified_name,
+ account2.qualified_name,
+ f"{currency.value}:bp_wall:{uuid4().hex}",
+ ]
+ )
+
+ assert isinstance(res, list)
+ assert len(res) == 2
+
+ # Confirm an empty array comes back for all unknown qualified names
+ res = lm.get_accounts_if_exists(
+ qualified_names=[
+ f"{lm.currency.value}:bp_wall:{uuid4().hex}" for i in range(5)
+ ]
+ )
+ assert isinstance(res, list)
+ assert len(res) == 0
+
+ def test_get_accounts_for_products(self, product_factory, thl_lm, lm):
+ from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerAccountDoesntExistError,
+ )
+ from generalresearch.models.thl.ledger import (
+ LedgerAccount,
+ )
+
+ # Create 5 Products
+ product_uuids = []
+ for i in range(5):
+ _p = product_factory()
+ product_uuids.append(_p.uuid)
+
+ # Confirm that this fails.. because none of those accounts have been
+ # created yet
+ with pytest.raises(expected_exception=LedgerAccountDoesntExistError):
+ thl_lm.get_accounts_bp_wallet_for_products(product_uuids=product_uuids)
+
+ # Create the bp_wallet accounts and then try again
+ for p_uuid in product_uuids:
+ thl_lm.get_account_or_create_bp_wallet_by_uuid(product_uuid=p_uuid)
+
+ res = thl_lm.get_accounts_bp_wallet_for_products(product_uuids=product_uuids)
+ assert len(res) == len(product_uuids)
+ assert all([isinstance(i, LedgerAccount) for i in res])
+
+
+class TestLedgerAccountManager:
+
+ def test_get_or_create(self, thl_lm, lm, lam):
+ from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerAccountDoesntExistError,
+ )
+ from generalresearch.models.thl.ledger import (
+ LedgerAccount,
+ Direction,
+ AccountType,
+ )
+
+ u = uuid4().hex
+ name = f"test-{u[:8]}"
+
+ account = LedgerAccount(
+ display_name=name,
+ qualified_name=f"test:bp_wallet:{u}",
+ normal_balance=Direction.DEBIT,
+ account_type=AccountType.BP_WALLET,
+ currency="test",
+ reference_type="bp",
+ reference_uuid=u,
+ )
+
+ # First we want to validate that using the get_account method raises
+ # an error for a random LedgerAccount which we know does not exist.
+ with pytest.raises(LedgerAccountDoesntExistError):
+ lam.get_account(qualified_name=account.qualified_name)
+
+ # Now that we know it doesn't exist, get_or_create for it
+ instance = lam.get_account_or_create(account=account)
+
+ # It should always return
+ assert isinstance(instance, LedgerAccount)
+ assert instance.reference_uuid == u
+
+ def test_get(self, user, thl_lm, lm, lam):
+ from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerAccountDoesntExistError,
+ )
+ from generalresearch.models.thl.ledger import (
+ LedgerAccount,
+ AccountType,
+ )
+
+ with pytest.raises(LedgerAccountDoesntExistError):
+ lam.get_account(qualified_name=f"test:bp_wallet:{user.product.id}")
+
+ thl_lm.get_account_or_create_bp_wallet(product=user.product)
+ account = lam.get_account(qualified_name=f"test:bp_wallet:{user.product.id}")
+
+ assert isinstance(account, LedgerAccount)
+ assert AccountType.BP_WALLET == account.account_type
+ assert user.product.uuid == account.reference_uuid
+
+ def test_get_many(self, product_factory, thl_lm, lm, lam, currency):
+ from generalresearch.models.thl.product import Product
+ from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerAccountDoesntExistError,
+ )
+
+ p1: Product = product_factory()
+ p2: Product = product_factory()
+
+ account1 = thl_lm.get_account_or_create_bp_wallet(product=p1)
+ account2 = thl_lm.get_account_or_create_bp_wallet(product=p2)
+
+ # Get 1 known account and confirm it comes back
+ res = lam.get_account_many(
+ qualified_names=[account1.qualified_name, account2.qualified_name]
+ )
+ assert isinstance(res, list)
+ assert len(res) == 2
+ assert account1 in res
+
+ # Get 2 known accounts and confirm they both come back
+ res = lam.get_account_many(
+ qualified_names=[account1.qualified_name, account2.qualified_name]
+ )
+ assert isinstance(res, list)
+ assert len(res) == 2
+ assert account1 in res
+ assert account2 in res
+
+ # Get 2 known and 1 made up qualified names, and confirm only 2 come
+ # back. Don't raise on error, so we can confirm the array is "short"
+ res = lam.get_account_many(
+ qualified_names=[
+ account1.qualified_name,
+ account2.qualified_name,
+ f"test:bp_wall:{uuid4().hex}",
+ ],
+ raise_on_error=False,
+ )
+ assert isinstance(res, list)
+ assert len(res) == 2
+
+ # Same as above, but confirm the raise works on checking res length
+ with pytest.raises(LedgerAccountDoesntExistError):
+ lam.get_account_many(
+ qualified_names=[
+ account1.qualified_name,
+ account2.qualified_name,
+ f"test:bp_wall:{uuid4().hex}",
+ ],
+ raise_on_error=True,
+ )
+
+ # Confirm an empty array comes back for all unknown qualified names
+ res = lam.get_account_many(
+ qualified_names=[f"test:bp_wall:{uuid4().hex}" for i in range(5)],
+ raise_on_error=False,
+ )
+ assert isinstance(res, list)
+ assert len(res) == 0
+
+ def test_create_account(self, thl_lm, lm, lam):
+ from generalresearch.models.thl.ledger import (
+ LedgerAccount,
+ Direction,
+ AccountType,
+ )
+
+ u = uuid4().hex
+ name = f"test-{u[:8]}"
+
+ account = LedgerAccount(
+ display_name=name,
+ qualified_name=f"test:bp_wallet:{u}",
+ normal_balance=Direction.DEBIT,
+ account_type=AccountType.BP_WALLET,
+ currency="test",
+ reference_type="bp",
+ reference_uuid=u,
+ )
+
+ lam.create_account(account=account)
+ assert lam.get_account(f"test:bp_wallet:{u}") == account
+ assert lam.get_account_or_create(account) == account
diff --git a/tests/managers/thl/test_ledger/test_thl_lm_bp_payout.py b/tests/managers/thl/test_ledger/test_thl_lm_bp_payout.py
new file mode 100644
index 0000000..294d092
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_thl_lm_bp_payout.py
@@ -0,0 +1,516 @@
+import logging
+from datetime import datetime, timezone, timedelta
+from decimal import Decimal
+from random import randint
+from uuid import uuid4
+
+import pytest
+import redis
+from pydantic import RedisDsn
+from redis.lock import Lock
+
+from generalresearch.currency import USDCent
+from generalresearch.managers.base import Permission
+from generalresearch.managers.thl.ledger_manager.thl_ledger import ThlLedgerManager
+from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerTransactionFlagAlreadyExistsError,
+ LedgerTransactionConditionFailedError,
+ LedgerTransactionReleaseLockError,
+ LedgerTransactionCreateError,
+)
+from generalresearch.managers.thl.ledger_manager.ledger import LedgerTransaction
+from generalresearch.models import Source
+from generalresearch.models.thl.definitions import PayoutStatus
+from generalresearch.models.thl.ledger import Direction, TransactionType
+from generalresearch.models.thl.session import (
+ Wall,
+ Status,
+ StatusCode1,
+ Session,
+)
+from generalresearch.models.thl.user import User
+from generalresearch.models.thl.wallet import PayoutType
+from generalresearch.redis_helper import RedisConfig
+
+
+def broken_acquire(self, *args, **kwargs):
+ raise redis.exceptions.TimeoutError("Simulated timeout during acquire")
+
+
+def broken_release(self, *args, **kwargs):
+ raise redis.exceptions.TimeoutError("Simulated timeout during release")
+
+
+class TestThlLedgerManagerBPPayout:
+
+ def test_create_tx_with_bp_payment(
+ self,
+ user_factory,
+ product_user_wallet_no,
+ create_main_accounts,
+ caplog,
+ thl_lm,
+ delete_ledger_db,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ now = datetime.now(timezone.utc) - timedelta(hours=1)
+ user: User = user_factory(product=product_user_wallet_no)
+
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.DYNATA,
+ req_survey_id="xxx",
+ req_cpi=Decimal("6.00"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=now,
+ finished=now + timedelta(seconds=1),
+ )
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall1, user=user, created=wall1.started
+ )
+ assert isinstance(tx, LedgerTransaction)
+
+ session = Session(started=wall1.started, user=user, wall_events=[wall1])
+ status, status_code_1 = session.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = session.determine_payments()
+ session.update(
+ **{
+ "status": status,
+ "status_code_1": status_code_1,
+ "finished": now + timedelta(minutes=10),
+ "payout": bp_pay,
+ "user_payout": user_pay,
+ }
+ )
+ thl_lm.create_tx_bp_payment(session=session, created=wall1.started)
+
+ lock_key = f"test:bp_payout:{user.product.id}"
+ flag_name = f"{thl_lm.cache_prefix}:transaction_flag:{lock_key}"
+ thl_lm.redis_client.delete(flag_name)
+
+ payoutevent_uuid = uuid4().hex
+ thl_lm.create_tx_bp_payout(
+ product=user.product,
+ amount=USDCent(200),
+ created=now,
+ payoutevent_uuid=payoutevent_uuid,
+ )
+
+ payoutevent_uuid = uuid4().hex
+ thl_lm.create_tx_bp_payout(
+ product=user.product,
+ amount=USDCent(200),
+ created=now + timedelta(minutes=2),
+ skip_one_per_day_check=True,
+ payoutevent_uuid=payoutevent_uuid,
+ )
+
+ cash = thl_lm.get_account_cash()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(user.product)
+ assert 170 == thl_lm.get_account_balance(bp_wallet_account)
+ assert 200 == thl_lm.get_account_balance(cash)
+
+ with pytest.raises(expected_exception=LedgerTransactionFlagAlreadyExistsError):
+ thl_lm.create_tx_bp_payout(
+ user.product,
+ amount=USDCent(200),
+ created=now + timedelta(minutes=2),
+ skip_one_per_day_check=False,
+ skip_wallet_balance_check=False,
+ payoutevent_uuid=payoutevent_uuid,
+ )
+
+ payoutevent_uuid = uuid4().hex
+ with caplog.at_level(logging.INFO):
+ with pytest.raises(LedgerTransactionConditionFailedError):
+ thl_lm.create_tx_bp_payout(
+ user.product,
+ amount=USDCent(10_000),
+ created=now + timedelta(minutes=2),
+ skip_one_per_day_check=True,
+ skip_wallet_balance_check=False,
+ payoutevent_uuid=payoutevent_uuid,
+ )
+ assert "failed condition check balance:" in caplog.text
+
+ thl_lm.create_tx_bp_payout(
+ product=user.product,
+ amount=USDCent(10_00),
+ created=now + timedelta(minutes=2),
+ skip_one_per_day_check=True,
+ skip_wallet_balance_check=True,
+ payoutevent_uuid=payoutevent_uuid,
+ )
+ assert 170 - 1000 == thl_lm.get_account_balance(bp_wallet_account)
+
+ def test_create_tx(self, product, caplog, thl_lm, currency):
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+ payoutevent_uuid = uuid4().hex
+
+ # Create a BP Payout for a Product without any activity. By issuing,
+ # the skip_* checks, we should be able to force it to work, and will
+ # then ultimately result in a negative balance
+ tx = thl_lm.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=payoutevent_uuid,
+ created=datetime.now(tz=timezone.utc),
+ skip_wallet_balance_check=True,
+ skip_one_per_day_check=True,
+ skip_flag_check=True,
+ )
+
+ # Check the basic attributes
+ assert isinstance(tx, LedgerTransaction)
+ assert tx.ext_description == "BP Payout"
+ assert (
+ tx.tag
+ == f"{currency.value}:{TransactionType.BP_PAYOUT.value}:{payoutevent_uuid}"
+ )
+ assert tx.entries[0].amount == rand_amount
+ assert tx.entries[1].amount == rand_amount
+
+ # Check the Product's balance, it should be negative the amount that was
+ # paid out. That's because the Product earned nothing.. and then was
+ # sent something.
+ balance = thl_lm.get_account_balance(
+ account=thl_lm.get_account_or_create_bp_wallet(product=product)
+ )
+ assert balance == int(rand_amount) * -1
+
+ # Test some basic assertions
+ with caplog.at_level(logging.INFO):
+ with pytest.raises(expected_exception=Exception):
+ thl_lm.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=uuid4().hex,
+ created=datetime.now(tz=timezone.utc),
+ skip_wallet_balance_check=False,
+ skip_one_per_day_check=False,
+ skip_flag_check=False,
+ )
+ assert "failed condition check >1 tx per day" in caplog.text
+
+ def test_create_tx_redis_failure(self, product, thl_web_rw, thl_lm):
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+ payoutevent_uuid = uuid4().hex
+ now = datetime.now(tz=timezone.utc)
+
+ thl_lm.create_tx_plug_bp_wallet(
+ product, rand_amount, now, direction=Direction.CREDIT
+ )
+
+ # Non routable IP address. Redis will fail
+ thl_lm_redis_0 = ThlLedgerManager(
+ pg_config=thl_web_rw,
+ permissions=[
+ Permission.CREATE,
+ Permission.READ,
+ Permission.UPDATE,
+ Permission.DELETE,
+ ],
+ testing=True,
+ redis_config=RedisConfig(
+ dsn=RedisDsn("redis://10.255.255.1:6379"),
+ socket_connect_timeout=0.1,
+ ),
+ )
+
+ with pytest.raises(expected_exception=Exception) as e:
+ tx = thl_lm_redis_0.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=payoutevent_uuid,
+ created=datetime.now(tz=timezone.utc),
+ )
+ assert e.type is redis.exceptions.TimeoutError
+ # No txs were created
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(product=product)
+ txs = thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet_account.uuid)
+ txs = [tx for tx in txs if tx.metadata["tx_type"] != "plug"]
+ assert len(txs) == 0
+
+ def test_create_tx_multiple_per_day(self, product, thl_lm):
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+ payoutevent_uuid = uuid4().hex
+ now = datetime.now(tz=timezone.utc)
+
+ thl_lm.create_tx_plug_bp_wallet(
+ product, rand_amount * USDCent(2), now, direction=Direction.CREDIT
+ )
+
+ tx = thl_lm.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=payoutevent_uuid,
+ created=datetime.now(tz=timezone.utc),
+ )
+
+ # Try to create another
+ # Will fail b/c it has the same payout event uuid
+ with pytest.raises(expected_exception=Exception) as e:
+ tx = thl_lm.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=payoutevent_uuid,
+ created=datetime.now(tz=timezone.utc),
+ )
+ assert e.type is LedgerTransactionFlagAlreadyExistsError
+
+ # Try to create another
+ # Will fail due to multiple per day
+ payoutevent_uuid2 = uuid4().hex
+ with pytest.raises(expected_exception=Exception) as e:
+ tx = thl_lm.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=payoutevent_uuid2,
+ created=datetime.now(tz=timezone.utc),
+ )
+ assert e.type is LedgerTransactionConditionFailedError
+ assert str(e.value) == ">1 tx per day"
+
+ # Make it run by skipping one per day check
+ tx = thl_lm.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=payoutevent_uuid2,
+ created=datetime.now(tz=timezone.utc),
+ skip_one_per_day_check=True,
+ )
+
+ def test_create_tx_redis_lock_release_error(self, product, thl_lm):
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+ payoutevent_uuid = uuid4().hex
+ now = datetime.now(tz=timezone.utc)
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(product=product)
+
+ thl_lm.create_tx_plug_bp_wallet(
+ product, rand_amount * USDCent(2), now, direction=Direction.CREDIT
+ )
+
+ original_acquire = Lock.acquire
+ original_release = Lock.release
+ Lock.acquire = broken_acquire
+
+ # Create TX will fail on lock enter, no tx will actually get created
+ with pytest.raises(expected_exception=Exception) as e:
+ tx = thl_lm.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=payoutevent_uuid,
+ created=datetime.now(tz=timezone.utc),
+ )
+ assert e.type is LedgerTransactionCreateError
+ assert str(e.value) == "Redis error: Simulated timeout during acquire"
+ txs = thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet_account.uuid)
+ txs = [tx for tx in txs if tx.metadata["tx_type"] != "plug"]
+ assert len(txs) == 0
+
+ Lock.acquire = original_acquire
+ Lock.release = broken_release
+
+ # Create TX will fail on lock exit, after the tx was created!
+ with pytest.raises(expected_exception=Exception) as e:
+ tx = thl_lm.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=payoutevent_uuid,
+ created=datetime.now(tz=timezone.utc),
+ )
+ assert e.type is LedgerTransactionReleaseLockError
+ assert str(e.value) == "Redis error: Simulated timeout during release"
+
+ # Transaction was still created!
+ txs = thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet_account.uuid)
+ txs = [tx for tx in txs if tx.metadata["tx_type"] != "plug"]
+ assert len(txs) == 1
+ Lock.release = original_release
+
+
+class TestPayoutEventManagerBPPayout:
+
+ def test_create(self, product, thl_lm, brokerage_product_payout_event_manager):
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+ now = datetime.now(tz=timezone.utc)
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(product=product)
+ assert thl_lm.get_account_balance(bp_wallet_account) == 0
+ thl_lm.create_tx_plug_bp_wallet(
+ product, rand_amount, now, direction=Direction.CREDIT
+ )
+ assert thl_lm.get_account_balance(bp_wallet_account) == rand_amount
+ brokerage_product_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm)
+
+ pe = brokerage_product_payout_event_manager.create_bp_payout_event(
+ thl_ledger_manager=thl_lm,
+ product=product,
+ created=now,
+ amount=rand_amount,
+ payout_type=PayoutType.ACH,
+ )
+ assert brokerage_product_payout_event_manager.check_for_ledger_tx(
+ thl_ledger_manager=thl_lm,
+ product_id=product.id,
+ amount=rand_amount,
+ payout_event=pe,
+ )
+ assert thl_lm.get_account_balance(bp_wallet_account) == 0
+
+ def test_create_with_redis_error(
+ self, product, caplog, thl_lm, brokerage_product_payout_event_manager
+ ):
+ caplog.set_level("WARNING")
+ original_acquire = Lock.acquire
+ original_release = Lock.release
+
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+ now = datetime.now(tz=timezone.utc)
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(product=product)
+ assert thl_lm.get_account_balance(bp_wallet_account) == 0
+ thl_lm.create_tx_plug_bp_wallet(
+ product, rand_amount, now, direction=Direction.CREDIT
+ )
+ assert thl_lm.get_account_balance(bp_wallet_account) == rand_amount
+ brokerage_product_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm)
+
+ # Will fail on lock enter, no tx will actually get created
+ Lock.acquire = broken_acquire
+ with pytest.raises(expected_exception=Exception) as e:
+ pe = brokerage_product_payout_event_manager.create_bp_payout_event(
+ thl_ledger_manager=thl_lm,
+ product=product,
+ created=now,
+ amount=rand_amount,
+ payout_type=PayoutType.ACH,
+ )
+ assert e.type is LedgerTransactionCreateError
+ assert str(e.value) == "Redis error: Simulated timeout during acquire"
+ assert any(
+ "Simulated timeout during acquire. No ledger tx was created" in m
+ for m in caplog.messages
+ )
+
+ txs = thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet_account.uuid)
+ txs = [tx for tx in txs if tx.metadata["tx_type"] != "plug"]
+ # One payout event is created, status is failed, and no ledger txs exist
+ assert len(txs) == 0
+ pes = (
+ brokerage_product_payout_event_manager.get_bp_bp_payout_events_for_products(
+ thl_ledger_manager=thl_lm, product_uuids=[product.id]
+ )
+ )
+ assert len(pes) == 1
+ assert pes[0].status == PayoutStatus.FAILED
+ pe = pes[0]
+
+ # Fix the redis method
+ Lock.acquire = original_acquire
+
+ # Try to fix the failed payout, by trying ledger tx again
+ brokerage_product_payout_event_manager.retry_create_bp_payout_event_tx(
+ product=product, thl_ledger_manager=thl_lm, payout_event_uuid=pe.uuid
+ )
+ txs = thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet_account.uuid)
+ txs = [tx for tx in txs if tx.metadata["tx_type"] != "plug"]
+ assert len(txs) == 1
+ assert thl_lm.get_account_balance(bp_wallet_account) == 0
+
+ # And then try to run it again, it'll fail because a payout event with the same info exists
+ with pytest.raises(expected_exception=Exception) as e:
+ pe = brokerage_product_payout_event_manager.create_bp_payout_event(
+ thl_ledger_manager=thl_lm,
+ product=product,
+ created=now,
+ amount=rand_amount,
+ payout_type=PayoutType.ACH,
+ )
+ assert e.type is ValueError
+ assert "Payout event already exists!" in str(e.value)
+
+ # We wouldn't do this in practice, because this is paying out the BP again, but
+ # we can if want to.
+ # Change the timestamp so it'll create a new payout event
+ now = datetime.now(tz=timezone.utc)
+ with pytest.raises(LedgerTransactionConditionFailedError) as e:
+ pe = brokerage_product_payout_event_manager.create_bp_payout_event(
+ thl_ledger_manager=thl_lm,
+ product=product,
+ created=now,
+ amount=rand_amount,
+ payout_type=PayoutType.ACH,
+ )
+ # But it will fail due to 1 per day check
+ assert str(e.value) == ">1 tx per day"
+ pe = brokerage_product_payout_event_manager.get_by_uuid(e.value.pe_uuid)
+ assert pe.status == PayoutStatus.FAILED
+
+ # And if we really want to, we can make it again
+ now = datetime.now(tz=timezone.utc)
+ pe = brokerage_product_payout_event_manager.create_bp_payout_event(
+ thl_ledger_manager=thl_lm,
+ product=product,
+ created=now,
+ amount=rand_amount,
+ payout_type=PayoutType.ACH,
+ skip_one_per_day_check=True,
+ skip_wallet_balance_check=True,
+ )
+
+ txs = thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet_account.uuid)
+ txs = [tx for tx in txs if tx.metadata["tx_type"] != "plug"]
+ assert len(txs) == 2
+ # since they were paid twice
+ assert thl_lm.get_account_balance(bp_wallet_account) == 0 - rand_amount
+
+ Lock.release = original_release
+ Lock.acquire = original_acquire
+
+ def test_create_with_redis_error_release(
+ self, product, caplog, thl_lm, brokerage_product_payout_event_manager
+ ):
+ caplog.set_level("WARNING")
+
+ original_release = Lock.release
+
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+ now = datetime.now(tz=timezone.utc)
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(product=product)
+ brokerage_product_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm)
+
+ assert thl_lm.get_account_balance(bp_wallet_account) == 0
+ thl_lm.create_tx_plug_bp_wallet(
+ product, rand_amount, now, direction=Direction.CREDIT
+ )
+ assert thl_lm.get_account_balance(bp_wallet_account) == rand_amount
+
+ # Will fail on lock exit, after the tx was created!
+ # But it'll see that the tx was created and so everything will be fine
+ Lock.release = broken_release
+ pe = brokerage_product_payout_event_manager.create_bp_payout_event(
+ thl_ledger_manager=thl_lm,
+ product=product,
+ created=now,
+ amount=rand_amount,
+ payout_type=PayoutType.ACH,
+ )
+ assert any(
+ "Simulated timeout during release but ledger tx exists" in m
+ for m in caplog.messages
+ )
+
+ txs = thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet_account.uuid)
+ txs = [tx for tx in txs if tx.metadata["tx_type"] != "plug"]
+ assert len(txs) == 1
+ pes = (
+ brokerage_product_payout_event_manager.get_bp_bp_payout_events_for_products(
+ thl_ledger_manager=thl_lm, product_uuids=[product.uuid]
+ )
+ )
+ assert len(pes) == 1
+ assert pes[0].status == PayoutStatus.COMPLETE
+ Lock.release = original_release
diff --git a/tests/managers/thl/test_ledger/test_thl_lm_tx.py b/tests/managers/thl/test_ledger/test_thl_lm_tx.py
new file mode 100644
index 0000000..31c7107
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_thl_lm_tx.py
@@ -0,0 +1,1762 @@
+import logging
+from datetime import datetime, timezone, timedelta
+from decimal import Decimal
+from random import randint
+from uuid import uuid4
+
+import pytest
+
+from generalresearch.currency import USDCent
+from generalresearch.managers.thl.ledger_manager.ledger import (
+ LedgerTransaction,
+)
+from generalresearch.models import Source
+from generalresearch.models.thl.definitions import (
+ WALL_ALLOWED_STATUS_STATUS_CODE,
+)
+from generalresearch.models.thl.ledger import Direction
+from generalresearch.models.thl.ledger import TransactionType
+from generalresearch.models.thl.product import (
+ PayoutConfig,
+ PayoutTransformation,
+ UserWalletConfig,
+)
+from generalresearch.models.thl.session import (
+ Wall,
+ Status,
+ StatusCode1,
+ Session,
+ WallAdjustedStatus,
+)
+from generalresearch.models.thl.user import User
+from generalresearch.models.thl.wallet import PayoutType
+from generalresearch.models.thl.payout import UserPayoutEvent
+
+logger = logging.getLogger("LedgerManager")
+
+
+class TestThlLedgerTxManager:
+
+ def test_create_tx_task_complete(
+ self,
+ wall,
+ user,
+ account_revenue_task_complete,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ ):
+ create_main_accounts()
+ tx = thl_lm.create_tx_task_complete(wall=wall, user=user)
+ assert isinstance(tx, LedgerTransaction)
+
+ res = lm.get_tx_by_id(transaction_id=tx.id)
+ assert res.created == tx.created
+
+ def test_create_tx_task_complete_(
+ self, wall, user, account_revenue_task_complete, thl_lm, lm
+ ):
+ tx = thl_lm.create_tx_task_complete_(wall=wall, user=user)
+ assert isinstance(tx, LedgerTransaction)
+
+ res = lm.get_tx_by_id(transaction_id=tx.id)
+ assert res.created == tx.created
+
+ def test_create_tx_bp_payment(
+ self,
+ session_factory,
+ user,
+ create_main_accounts,
+ delete_ledger_db,
+ thl_lm,
+ lm,
+ session_manager,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+ s1 = session_factory(user=user)
+
+ status, status_code_1 = s1.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = s1.determine_payments()
+ session_manager.finish_with_status(
+ session=s1,
+ status=Status.COMPLETE,
+ status_code_1=status_code_1,
+ finished=datetime.now(tz=timezone.utc) + timedelta(minutes=10),
+ payout=bp_pay,
+ user_payout=user_pay,
+ )
+
+ tx = thl_lm.create_tx_bp_payment(session=s1)
+ assert isinstance(tx, LedgerTransaction)
+
+ res = lm.get_tx_by_id(transaction_id=tx.id)
+ assert res.created == tx.created
+
+ def test_create_tx_bp_payment_amt(
+ self,
+ session_factory,
+ user_factory,
+ product_manager,
+ create_main_accounts,
+ delete_ledger_db,
+ thl_lm,
+ lm,
+ session_manager,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+ product = product_manager.create_dummy(
+ payout_config=PayoutConfig(
+ payout_transformation=PayoutTransformation(
+ f="payout_transformation_amt"
+ )
+ ),
+ user_wallet_config=UserWalletConfig(amt=True, enabled=True),
+ )
+ user = user_factory(product=product)
+ s1 = session_factory(user=user, wall_req_cpi=Decimal("1"))
+
+ status, status_code_1 = s1.determine_session_status()
+ assert status == Status.COMPLETE
+ thl_net, commission_amount, bp_pay, user_pay = s1.determine_payments(
+ thl_ledger_manager=thl_lm
+ )
+ print(thl_net, commission_amount, bp_pay, user_pay)
+ session_manager.finish_with_status(
+ session=s1,
+ status=Status.COMPLETE,
+ status_code_1=status_code_1,
+ finished=datetime.now(tz=timezone.utc) + timedelta(minutes=10),
+ payout=bp_pay,
+ user_payout=user_pay,
+ )
+
+ tx = thl_lm.create_tx_bp_payment(session=s1)
+ assert isinstance(tx, LedgerTransaction)
+
+ res = lm.get_tx_by_id(transaction_id=tx.id)
+ assert res.created == tx.created
+
+ def test_create_tx_bp_payment_(
+ self,
+ session_factory,
+ user,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ session_manager,
+ utc_hour_ago,
+ ):
+ s1 = session_factory(user=user)
+ status, status_code_1 = s1.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = s1.determine_payments()
+ session_manager.finish_with_status(
+ session=s1,
+ status=status,
+ status_code_1=status_code_1,
+ finished=utc_hour_ago + timedelta(minutes=10),
+ payout=bp_pay,
+ user_payout=user_pay,
+ )
+
+ s1.determine_payments()
+ tx = thl_lm.create_tx_bp_payment_(session=s1)
+ assert isinstance(tx, LedgerTransaction)
+
+ res = lm.get_tx_by_id(transaction_id=tx.id)
+ assert res.created == tx.created
+
+ def test_create_tx_task_adjustment(
+ self, wall_factory, session, user, create_main_accounts, thl_lm, lm
+ ):
+ """Create Wall event Complete, and Create a Tx Task Adjustment
+
+ - I don't know what this does exactly... but we can confirm
+ the transaction comes back with balanced amounts, and that
+ the name of the Source is in the Tx description
+ """
+
+ wall_status = Status.COMPLETE
+ wall: Wall = wall_factory(session=session, wall_status=wall_status)
+
+ tx = thl_lm.create_tx_task_adjustment(wall=wall, user=user)
+ assert isinstance(tx, LedgerTransaction)
+ res = lm.get_tx_by_id(transaction_id=tx.id)
+
+ assert res.entries[0].amount == int(wall.cpi * 100)
+ assert res.entries[1].amount == int(wall.cpi * 100)
+ assert wall.source.name in res.ext_description
+ assert res.created == tx.created
+
+ def test_create_tx_bp_adjustment(self, session, user, caplog, thl_lm, lm):
+ status, status_code_1 = session.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = session.determine_payments()
+
+ # The default session fixture is just an unfinished wall event
+ assert len(session.wall_events) == 1
+ assert session.finished is None
+ assert status == Status.TIMEOUT
+ assert status_code_1 in list(
+ WALL_ALLOWED_STATUS_STATUS_CODE.get(Status.TIMEOUT, {})
+ )
+ assert thl_net == Decimal(0)
+ assert commission_amount == Decimal(0)
+ assert bp_pay == Decimal(0)
+ assert user_pay is None
+
+ # Update the finished timestamp, but nothing else. This means that
+ # there is no financial changes needed
+ session.update(
+ **{
+ "finished": datetime.now(tz=timezone.utc) + timedelta(minutes=10),
+ }
+ )
+ assert session.finished
+ with caplog.at_level(logging.INFO):
+ tx = thl_lm.create_tx_bp_adjustment(session=session)
+ assert tx is None
+ assert "No transactions needed." in caplog.text
+
+ def test_create_tx_bp_payout(self, product, caplog, thl_lm, currency):
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+ payoutevent_uuid = uuid4().hex
+
+ # Create a BP Payout for a Product without any activity. By issuing,
+ # the skip_* checks, we should be able to force it to work, and will
+ # then ultimately result in a negative balance
+ tx = thl_lm.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=payoutevent_uuid,
+ created=datetime.now(tz=timezone.utc),
+ skip_wallet_balance_check=True,
+ skip_one_per_day_check=True,
+ skip_flag_check=True,
+ )
+
+ # Check the basic attributes
+ assert isinstance(tx, LedgerTransaction)
+ assert tx.ext_description == "BP Payout"
+ assert (
+ tx.tag
+ == f"{thl_lm.currency.value}:{TransactionType.BP_PAYOUT.value}:{payoutevent_uuid}"
+ )
+ assert tx.entries[0].amount == rand_amount
+ assert tx.entries[1].amount == rand_amount
+
+ # Check the Product's balance, it should be negative the amount that was
+ # paid out. That's because the Product earned nothing.. and then was
+ # sent something.
+ balance = thl_lm.get_account_balance(
+ account=thl_lm.get_account_or_create_bp_wallet(product=product)
+ )
+ assert balance == int(rand_amount) * -1
+
+ # Test some basic assertions
+ with caplog.at_level(logging.INFO):
+ with pytest.raises(expected_exception=Exception):
+ thl_lm.create_tx_bp_payout(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=uuid4().hex,
+ created=datetime.now(tz=timezone.utc),
+ skip_wallet_balance_check=False,
+ skip_one_per_day_check=False,
+ skip_flag_check=False,
+ )
+ assert "failed condition check >1 tx per day" in caplog.text
+
+ def test_create_tx_bp_payout_(self, product, thl_lm, lm, currency):
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+ payoutevent_uuid = uuid4().hex
+
+ # Create a BP Payout for a Product without any activity.
+ tx = thl_lm.create_tx_bp_payout_(
+ product=product,
+ amount=rand_amount,
+ payoutevent_uuid=payoutevent_uuid,
+ created=datetime.now(tz=timezone.utc),
+ )
+
+ # Check the basic attributes
+ assert isinstance(tx, LedgerTransaction)
+ assert tx.ext_description == "BP Payout"
+ assert (
+ tx.tag
+ == f"{currency.value}:{TransactionType.BP_PAYOUT.value}:{payoutevent_uuid}"
+ )
+ assert tx.entries[0].amount == rand_amount
+ assert tx.entries[1].amount == rand_amount
+
+ def test_create_tx_plug_bp_wallet(
+ self, product, create_main_accounts, thl_lm, lm, currency
+ ):
+ """A BP Wallet "plug" is a way to makeup discrepancies and simply
+ add or remove money
+ """
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+
+ tx = thl_lm.create_tx_plug_bp_wallet(
+ product=product,
+ amount=rand_amount,
+ created=datetime.now(tz=timezone.utc),
+ direction=Direction.DEBIT,
+ skip_flag_check=False,
+ )
+
+ assert isinstance(tx, LedgerTransaction)
+
+ # We issued the BP money they didn't earn, so now they have a
+ # negative balance
+ balance = thl_lm.get_account_balance(
+ account=thl_lm.get_account_or_create_bp_wallet(product=product)
+ )
+ assert balance == int(rand_amount) * -1
+
+ def test_create_tx_plug_bp_wallet_(
+ self, product, create_main_accounts, thl_lm, lm, currency
+ ):
+ """A BP Wallet "plug" is a way to fix discrepancies and simply
+ add or remove money.
+
+ Similar to above, but because it's unprotected, we can immediately
+ issue another to see if the balance changes
+ """
+ rand_amount: USDCent = USDCent(randint(100, 1_000))
+
+ tx = thl_lm.create_tx_plug_bp_wallet_(
+ product=product,
+ amount=rand_amount,
+ created=datetime.now(tz=timezone.utc),
+ direction=Direction.DEBIT,
+ )
+
+ assert isinstance(tx, LedgerTransaction)
+
+ # We issued the BP money they didn't earn, so now they have a
+ # negative balance
+ balance = thl_lm.get_account_balance(
+ account=thl_lm.get_account_or_create_bp_wallet(product=product)
+ )
+ assert balance == int(rand_amount) * -1
+
+ # Issue a positive one now, and confirm the balance goes positive
+ thl_lm.create_tx_plug_bp_wallet_(
+ product=product,
+ amount=rand_amount + rand_amount,
+ created=datetime.now(tz=timezone.utc),
+ direction=Direction.CREDIT,
+ )
+ balance = thl_lm.get_account_balance(
+ account=thl_lm.get_account_or_create_bp_wallet(product=product)
+ )
+ assert balance == int(rand_amount)
+
+ def test_create_tx_user_payout_request(
+ self,
+ user,
+ product_user_wallet_yes,
+ user_factory,
+ delete_df_collection,
+ thl_lm,
+ lm,
+ currency,
+ ):
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.PAYPAL,
+ amount=500,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+
+ # The default user fixture uses a product that doesn't have wallet
+ # mode enabled
+ with pytest.raises(expected_exception=AssertionError):
+ thl_lm.create_tx_user_payout_request(
+ user=user,
+ payout_event=pe,
+ skip_flag_check=True,
+ skip_wallet_balance_check=True,
+ )
+
+ # Now try it for a user on a product with wallet mode
+ u2 = user_factory(product=product_user_wallet_yes)
+
+ # User's pre-balance is 0 because no activity has occurred yet
+ pre_balance = lm.get_account_balance(
+ account=thl_lm.get_account_or_create_user_wallet(user=u2)
+ )
+ assert pre_balance == 0
+
+ tx = thl_lm.create_tx_user_payout_request(
+ user=u2,
+ payout_event=pe,
+ skip_flag_check=True,
+ skip_wallet_balance_check=True,
+ )
+ assert isinstance(tx, LedgerTransaction)
+ assert tx.entries[0].amount == pe.amount
+ assert tx.entries[1].amount == pe.amount
+ assert tx.ext_description == "User Payout Paypal Request $5.00"
+
+ #
+ # (TODO): This key ":user_payout:" is NOT part of the TransactionType
+ # enum and was manually set. It should be based off the
+ # TransactionType names.
+ #
+
+ assert tx.tag == f"{currency.value}:user_payout:{pe.uuid}:request"
+
+ # Post balance is -$5.00 because it comes out of the wallet before
+ # it's Approved or Completed
+ post_balance = lm.get_account_balance(
+ account=thl_lm.get_account_or_create_user_wallet(user=u2)
+ )
+ assert post_balance == -500
+
+ def test_create_tx_user_payout_request_(
+ self,
+ user,
+ product_user_wallet_yes,
+ user_factory,
+ delete_ledger_db,
+ thl_lm,
+ lm,
+ ):
+ delete_ledger_db()
+
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.PAYPAL,
+ amount=500,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+
+ rand_description = uuid4().hex
+ tx = thl_lm.create_tx_user_payout_request_(
+ user=user, payout_event=pe, description=rand_description
+ )
+
+ assert tx.ext_description == rand_description
+
+ post_balance = lm.get_account_balance(
+ account=thl_lm.get_account_or_create_user_wallet(user=user)
+ )
+ assert post_balance == -500
+
+ def test_create_tx_user_payout_complete(
+ self,
+ user_factory,
+ product_user_wallet_yes,
+ create_main_accounts,
+ delete_ledger_db,
+ thl_lm,
+ lm,
+ currency,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ user: User = user_factory(product=product_user_wallet_yes)
+ user_account = thl_lm.get_account_or_create_user_wallet(user=user)
+ rand_amount = randint(100, 1_000)
+
+ # Ensure the user starts out with nothing...
+ assert lm.get_account_balance(account=user_account) == 0
+
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.PAYPAL,
+ amount=rand_amount,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+
+ # Confirm it's not possible unless a request occurred happen
+ with pytest.raises(expected_exception=ValueError):
+ thl_lm.create_tx_user_payout_complete(
+ user=user,
+ payout_event=pe,
+ fee_amount=None,
+ skip_flag_check=False,
+ )
+
+ # (1) Make a request first
+ thl_lm.create_tx_user_payout_request(
+ user=user,
+ payout_event=pe,
+ skip_flag_check=True,
+ skip_wallet_balance_check=True,
+ )
+ # Assert the balance came out of their user wallet
+ assert lm.get_account_balance(account=user_account) == rand_amount * -1
+
+ # (2) Complete the request
+ tx = thl_lm.create_tx_user_payout_complete(
+ user=user,
+ payout_event=pe,
+ fee_amount=Decimal(0),
+ skip_flag_check=False,
+ )
+ assert tx.entries[0].amount == rand_amount
+ assert tx.entries[1].amount == rand_amount
+ assert tx.tag == f"{currency.value}:user_payout:{pe.uuid}:complete"
+ assert isinstance(tx, LedgerTransaction)
+
+ # The amount that comes out of the user wallet doesn't change after
+ # it's approved becuase it's already been withdrawn
+ assert lm.get_account_balance(account=user_account) == rand_amount * -1
+
+ def test_create_tx_user_payout_complete_(
+ self,
+ user_factory,
+ product_user_wallet_yes,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ ):
+ user: User = user_factory(product=product_user_wallet_yes)
+ user_account = thl_lm.get_account_or_create_user_wallet(user=user)
+ rand_amount = randint(100, 1_000)
+
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.PAYPAL,
+ amount=rand_amount,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+
+ # (1) Make a request first
+ thl_lm.create_tx_user_payout_request(
+ user=user,
+ payout_event=pe,
+ skip_flag_check=True,
+ skip_wallet_balance_check=True,
+ )
+
+ # (2) Complete the request
+ rand_desc = uuid4().hex
+
+ bp_expense_account = thl_lm.get_account_or_create_bp_expense(
+ product=user.product, expense_name="paypal"
+ )
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(product=user.product)
+
+ tx = thl_lm.create_tx_user_payout_complete_(
+ user=user,
+ payout_event=pe,
+ fee_amount=Decimal("0.00"),
+ fee_expense_account=bp_expense_account,
+ fee_payer_account=bp_wallet_account,
+ description=rand_desc,
+ )
+ assert tx.ext_description == rand_desc
+ assert lm.get_account_balance(account=user_account) == rand_amount * -1
+
+ def test_create_tx_user_payout_cancelled(
+ self,
+ user_factory,
+ product_user_wallet_yes,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ currency,
+ ):
+ user: User = user_factory(product=product_user_wallet_yes)
+ user_account = thl_lm.get_account_or_create_user_wallet(user=user)
+ rand_amount = randint(100, 1_000)
+
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.PAYPAL,
+ amount=rand_amount,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+
+ # (1) Make a request first
+ thl_lm.create_tx_user_payout_request(
+ user=user,
+ payout_event=pe,
+ skip_flag_check=True,
+ skip_wallet_balance_check=True,
+ )
+ # Assert the balance came out of their user wallet
+ assert lm.get_account_balance(account=user_account) == rand_amount * -1
+
+ # (2) Cancel the request
+ tx = thl_lm.create_tx_user_payout_cancelled(
+ user=user,
+ payout_event=pe,
+ skip_flag_check=False,
+ )
+ assert tx.entries[0].amount == rand_amount
+ assert tx.entries[1].amount == rand_amount
+ assert tx.tag == f"{currency.value}:user_payout:{pe.uuid}:cancel"
+ assert isinstance(tx, LedgerTransaction)
+
+ # Assert the balance comes back to 0 after it was cancelled
+ assert lm.get_account_balance(account=user_account) == 0
+
+ def test_create_tx_user_payout_cancelled_(
+ self,
+ user_factory,
+ product_user_wallet_yes,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ currency,
+ ):
+ user: User = user_factory(product=product_user_wallet_yes)
+ user_account = thl_lm.get_account_or_create_user_wallet(user=user)
+ rand_amount = randint(100, 1_000)
+
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.PAYPAL,
+ amount=rand_amount,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+
+ # (1) Make a request first
+ thl_lm.create_tx_user_payout_request(
+ user=user,
+ payout_event=pe,
+ skip_flag_check=True,
+ skip_wallet_balance_check=True,
+ )
+ # Assert the balance came out of their user wallet
+ assert lm.get_account_balance(account=user_account) == rand_amount * -1
+
+ # (2) Cancel the request
+ rand_desc = uuid4().hex
+ tx = thl_lm.create_tx_user_payout_cancelled_(
+ user=user, payout_event=pe, description=rand_desc
+ )
+ assert isinstance(tx, LedgerTransaction)
+ assert tx.ext_description == rand_desc
+ assert lm.get_account_balance(account=user_account) == 0
+
+ def test_create_tx_user_bonus(
+ self,
+ user_factory,
+ product_user_wallet_yes,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ currency,
+ ):
+ user: User = user_factory(product=product_user_wallet_yes)
+ user_account = thl_lm.get_account_or_create_user_wallet(user=user)
+ rand_amount = randint(100, 1_000)
+ rand_ref_uuid = uuid4().hex
+ rand_desc = uuid4().hex
+
+ # Assert the balance came out of their user wallet
+ assert lm.get_account_balance(account=user_account) == 0
+
+ tx = thl_lm.create_tx_user_bonus(
+ user=user,
+ amount=Decimal(rand_amount / 100),
+ ref_uuid=rand_ref_uuid,
+ description=rand_desc,
+ skip_flag_check=True,
+ )
+ assert tx.ext_description == rand_desc
+ assert tx.tag == f"{thl_lm.currency.value}:user_bonus:{rand_ref_uuid}"
+ assert tx.entries[0].amount == rand_amount
+ assert tx.entries[1].amount == rand_amount
+
+ # Assert the balance came out of their user wallet
+ assert lm.get_account_balance(account=user_account) == rand_amount
+
+ def test_create_tx_user_bonus_(
+ self,
+ user_factory,
+ product_user_wallet_yes,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ currency,
+ ):
+ user: User = user_factory(product=product_user_wallet_yes)
+ user_account = thl_lm.get_account_or_create_user_wallet(user=user)
+ rand_amount = randint(100, 1_000)
+ rand_ref_uuid = uuid4().hex
+ rand_desc = uuid4().hex
+
+ # Assert the balance came out of their user wallet
+ assert lm.get_account_balance(account=user_account) == 0
+
+ tx = thl_lm.create_tx_user_bonus_(
+ user=user,
+ amount=Decimal(rand_amount / 100),
+ ref_uuid=rand_ref_uuid,
+ description=rand_desc,
+ )
+ assert tx.ext_description == rand_desc
+ assert tx.tag == f"{thl_lm.currency.value}:user_bonus:{rand_ref_uuid}"
+ assert tx.entries[0].amount == rand_amount
+ assert tx.entries[1].amount == rand_amount
+
+ # Assert the balance came out of their user wallet
+ assert lm.get_account_balance(account=user_account) == rand_amount
+
+
+class TestThlLedgerTxManagerFlows:
+ """Combine the various THL_LM methods to create actual "real world"
+ examples
+ """
+
+ def test_create_tx_task_complete(
+ self, user, create_main_accounts, thl_lm, lm, currency, delete_ledger_db
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ wall1 = Wall(
+ user_id=1,
+ source=Source.DYNATA,
+ req_survey_id="xxx",
+ req_cpi=Decimal("1.23"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=datetime.now(timezone.utc),
+ finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ )
+ thl_lm.create_tx_task_complete(wall=wall1, user=user, created=wall1.started)
+
+ wall2 = Wall(
+ user_id=1,
+ source=Source.FULL_CIRCLE,
+ req_survey_id="yyy",
+ req_cpi=Decimal("3.21"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=datetime.now(timezone.utc),
+ finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ )
+ thl_lm.create_tx_task_complete(wall=wall2, user=user, created=wall2.started)
+
+ cash = thl_lm.get_account_cash()
+ revenue = thl_lm.get_account_task_complete_revenue()
+
+ assert lm.get_account_balance(cash) == 123 + 321
+ assert lm.get_account_balance(revenue) == 123 + 321
+ assert lm.check_ledger_balanced()
+
+ assert (
+ lm.get_account_filtered_balance(
+ account=revenue, metadata_key="source", metadata_value="d"
+ )
+ == 123
+ )
+
+ assert (
+ lm.get_account_filtered_balance(
+ account=revenue, metadata_key="source", metadata_value="f"
+ )
+ == 321
+ )
+
+ assert (
+ lm.get_account_filtered_balance(
+ account=revenue, metadata_key="source", metadata_value="x"
+ )
+ == 0
+ )
+
+ assert (
+ thl_lm.get_account_filtered_balance(
+ account=revenue,
+ metadata_key="thl_wall",
+ metadata_value=wall1.uuid,
+ )
+ == 123
+ )
+
+ def test_create_transaction_task_complete_1_cent(
+ self, user, create_main_accounts, thl_lm, lm, currency
+ ):
+ wall1 = Wall(
+ user_id=1,
+ source=Source.DYNATA,
+ req_survey_id="xxx",
+ req_cpi=Decimal("0.007"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=datetime.now(timezone.utc),
+ finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ )
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall1, user=user, created=wall1.started
+ )
+
+ assert isinstance(tx, LedgerTransaction)
+
+ def test_create_transaction_bp_payment(
+ self,
+ user,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ currency,
+ delete_ledger_db,
+ session_factory,
+ utc_hour_ago,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ s1: Session = session_factory(
+ user=user,
+ wall_count=1,
+ started=utc_hour_ago,
+ wall_source=Source.TESTING,
+ )
+ w1: Wall = s1.wall_events[0]
+
+ tx = thl_lm.create_tx_task_complete(wall=w1, user=user, created=w1.started)
+ assert isinstance(tx, LedgerTransaction)
+
+ status, status_code_1 = s1.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = s1.determine_payments()
+ s1.update(
+ **{
+ "status": status,
+ "status_code_1": status_code_1,
+ "finished": s1.started + timedelta(minutes=10),
+ "payout": bp_pay,
+ "user_payout": user_pay,
+ }
+ )
+ print(thl_net, commission_amount, bp_pay, user_pay)
+ thl_lm.create_tx_bp_payment(session=s1, created=w1.started)
+
+ revenue = thl_lm.get_account_task_complete_revenue()
+ bp_wallet = thl_lm.get_account_or_create_bp_wallet(product=user.product)
+ bp_commission = thl_lm.get_account_or_create_bp_commission(product=user.product)
+
+ assert 0 == lm.get_account_balance(account=revenue)
+ assert 50 == lm.get_account_filtered_balance(
+ account=revenue,
+ metadata_key="source",
+ metadata_value=Source.TESTING,
+ )
+ assert 48 == lm.get_account_balance(account=bp_wallet)
+ assert 48 == lm.get_account_filtered_balance(
+ account=bp_wallet,
+ metadata_key="thl_session",
+ metadata_value=s1.uuid,
+ )
+ assert 2 == thl_lm.get_account_balance(account=bp_commission)
+ assert thl_lm.check_ledger_balanced()
+
+ def test_create_transaction_bp_payment_round(
+ self,
+ user_factory,
+ product_user_wallet_no,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ currency,
+ ):
+ product_user_wallet_no.commission_pct = Decimal("0.085")
+ user: User = user_factory(product=product_user_wallet_no)
+
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.SAGO,
+ req_survey_id="xxx",
+ req_cpi=Decimal("0.287"),
+ session_id=3,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=datetime.now(timezone.utc),
+ finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ )
+
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall1, user=user, created=wall1.started
+ )
+ assert isinstance(tx, LedgerTransaction)
+
+ session = Session(started=wall1.started, user=user, wall_events=[wall1])
+ status, status_code_1 = session.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = session.determine_payments()
+ session.update(
+ **{
+ "status": status,
+ "status_code_1": status_code_1,
+ "finished": session.started + timedelta(minutes=10),
+ "payout": bp_pay,
+ "user_payout": user_pay,
+ }
+ )
+
+ print(thl_net, commission_amount, bp_pay, user_pay)
+ tx = thl_lm.create_tx_bp_payment(session=session, created=wall1.started)
+ assert isinstance(tx, LedgerTransaction)
+
+ def test_create_transaction_bp_payment_round2(
+ self, delete_ledger_db, user, create_main_accounts, thl_lm, lm, currency
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+ # user must be no user wallet
+ # e.g. session 869b5bfa47f44b4f81cd095ed01df2ff this fails if you dont round properly
+
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.SAGO,
+ req_survey_id="xxx",
+ req_cpi=Decimal("1.64500"),
+ session_id=3,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=datetime.now(timezone.utc),
+ finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ )
+
+ thl_lm.create_tx_task_complete(wall=wall1, user=user, created=wall1.started)
+ session = Session(started=wall1.started, user=user, wall_events=[wall1])
+ status, status_code_1 = session.determine_session_status()
+ # thl_net, commission_amount, bp_pay, user_pay = session.determine_payments()
+ session.update(
+ **{
+ "status": status,
+ "status_code_1": status_code_1,
+ "finished": session.started + timedelta(minutes=10),
+ "payout": Decimal("1.53"),
+ "user_payout": Decimal("1.53"),
+ }
+ )
+
+ thl_lm.create_tx_bp_payment(session=session, created=wall1.started)
+
+ def test_create_transaction_bp_payment_round3(
+ self,
+ user_factory,
+ product_user_wallet_yes,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ currency,
+ ):
+ # e.g. session ___ fails b/c we rounded incorrectly
+ # before, and now we are off by a penny...
+ user: User = user_factory(product=product_user_wallet_yes)
+
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.SAGO,
+ req_survey_id="xxx",
+ req_cpi=Decimal("0.385"),
+ session_id=3,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=datetime.now(timezone.utc),
+ finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ )
+ thl_lm.create_tx_task_complete(wall=wall1, user=user, created=wall1.started)
+
+ session = Session(started=wall1.started, user=user, wall_events=[wall1])
+ status, status_code_1 = session.determine_session_status()
+ # thl_net, commission_amount, bp_pay, user_pay = session.determine_payments()
+ session.update(
+ **{
+ "status": status,
+ "status_code_1": status_code_1,
+ "finished": session.started + timedelta(minutes=10),
+ "payout": Decimal("0.39"),
+ "user_payout": Decimal("0.26"),
+ }
+ )
+ # with pytest.logs(logger, level=logging.WARNING) as cm:
+ # tx = thl_lm.create_transaction_bp_payment(session, created=wall1.started)
+ # assert "Capping bp_pay to thl_net" in cm.output[0]
+
+ def test_create_transaction_bp_payment_user_wallet(
+ self,
+ user_factory,
+ product_user_wallet_yes,
+ create_main_accounts,
+ delete_ledger_db,
+ thl_lm,
+ session_manager,
+ wall_manager,
+ lm,
+ session_factory,
+ currency,
+ utc_hour_ago,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ user: User = user_factory(product=product_user_wallet_yes)
+ assert user.product.user_wallet_enabled
+
+ s1: Session = session_factory(
+ user=user,
+ wall_count=1,
+ started=utc_hour_ago,
+ wall_req_cpi=Decimal(".50"),
+ wall_source=Source.TESTING,
+ )
+ w1: Wall = s1.wall_events[0]
+
+ thl_lm.create_tx_task_complete(wall=w1, user=user, created=w1.started)
+
+ status, status_code_1 = s1.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = s1.determine_payments()
+ session_manager.finish_with_status(
+ session=s1,
+ status=status,
+ status_code_1=status_code_1,
+ finished=s1.started + timedelta(minutes=10),
+ payout=bp_pay,
+ user_payout=user_pay,
+ )
+ thl_lm.create_tx_bp_payment(session=s1, created=w1.started)
+
+ revenue = thl_lm.get_account_task_complete_revenue()
+ bp_wallet = thl_lm.get_account_or_create_bp_wallet(product=user.product)
+ bp_commission = thl_lm.get_account_or_create_bp_commission(product=user.product)
+ user_wallet = thl_lm.get_account_or_create_user_wallet(user=user)
+
+ assert 0 == thl_lm.get_account_balance(account=revenue)
+ assert 50 == thl_lm.get_account_filtered_balance(
+ account=revenue,
+ metadata_key="source",
+ metadata_value=Source.TESTING,
+ )
+
+ assert 48 - 19 == thl_lm.get_account_balance(account=bp_wallet)
+ assert 48 - 19 == thl_lm.get_account_filtered_balance(
+ account=bp_wallet,
+ metadata_key="thl_session",
+ metadata_value=s1.uuid,
+ )
+ assert 2 == thl_lm.get_account_balance(bp_commission)
+ assert 19 == thl_lm.get_account_balance(user_wallet)
+ assert 19 == thl_lm.get_account_filtered_balance(
+ account=user_wallet,
+ metadata_key="thl_session",
+ metadata_value=s1.uuid,
+ )
+
+ assert 0 == thl_lm.get_account_filtered_balance(
+ account=user_wallet, metadata_key="thl_session", metadata_value="x"
+ )
+ assert thl_lm.check_ledger_balanced()
+
+
+class TestThlLedgerManagerAdj:
+
+ def test_create_tx_task_adjustment(
+ self,
+ user_factory,
+ product_user_wallet_no,
+ create_main_accounts,
+ delete_ledger_db,
+ thl_lm,
+ lm,
+ utc_hour_ago,
+ currency,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ user: User = user_factory(product=product_user_wallet_no)
+
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.DYNATA,
+ req_survey_id="xxx",
+ req_cpi=Decimal("1.23"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=utc_hour_ago,
+ finished=utc_hour_ago + timedelta(seconds=1),
+ )
+
+ thl_lm.create_tx_task_complete(wall1, user, created=wall1.started)
+
+ wall2 = Wall(
+ user_id=1,
+ source=Source.FULL_CIRCLE,
+ req_survey_id="yyy",
+ req_cpi=Decimal("3.21"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=utc_hour_ago,
+ finished=utc_hour_ago + timedelta(seconds=1),
+ )
+ thl_lm.create_tx_task_complete(wall2, user, created=wall2.started)
+
+ wall1.update(
+ adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL,
+ adjusted_cpi=0,
+ adjusted_timestamp=utc_hour_ago + timedelta(hours=1),
+ )
+ print(wall1.get_cpi_after_adjustment())
+ thl_lm.create_tx_task_adjustment(wall1, user)
+
+ cash = thl_lm.get_account_cash()
+ revenue = thl_lm.get_account_task_complete_revenue()
+
+ assert 123 + 321 - 123 == thl_lm.get_account_balance(account=cash)
+ assert 123 + 321 - 123 == thl_lm.get_account_balance(account=revenue)
+ assert thl_lm.check_ledger_balanced()
+ assert 0 == thl_lm.get_account_filtered_balance(
+ revenue, metadata_key="source", metadata_value="d"
+ )
+ assert 321 == thl_lm.get_account_filtered_balance(
+ revenue, metadata_key="source", metadata_value="f"
+ )
+ assert 0 == thl_lm.get_account_filtered_balance(
+ revenue, metadata_key="source", metadata_value="x"
+ )
+ assert 123 - 123 == thl_lm.get_account_filtered_balance(
+ account=revenue, metadata_key="thl_wall", metadata_value=wall1.uuid
+ )
+
+ # un-reconcile it
+ wall1.update(
+ adjusted_status=None,
+ adjusted_cpi=None,
+ adjusted_timestamp=utc_hour_ago + timedelta(minutes=45),
+ )
+ print(wall1.get_cpi_after_adjustment())
+ thl_lm.create_tx_task_adjustment(wall1, user)
+ # and then run it again to make sure it does nothing
+ thl_lm.create_tx_task_adjustment(wall1, user)
+
+ cash = thl_lm.get_account_cash()
+ revenue = thl_lm.get_account_task_complete_revenue()
+
+ assert 123 + 321 - 123 + 123 == thl_lm.get_account_balance(cash)
+ assert 123 + 321 - 123 + 123 == thl_lm.get_account_balance(revenue)
+ assert thl_lm.check_ledger_balanced()
+ assert 123 == thl_lm.get_account_filtered_balance(
+ account=revenue, metadata_key="source", metadata_value="d"
+ )
+ assert 321 == thl_lm.get_account_filtered_balance(
+ account=revenue, metadata_key="source", metadata_value="f"
+ )
+ assert 0 == thl_lm.get_account_filtered_balance(
+ account=revenue, metadata_key="source", metadata_value="x"
+ )
+ assert 123 - 123 + 123 == thl_lm.get_account_filtered_balance(
+ account=revenue, metadata_key="thl_wall", metadata_value=wall1.uuid
+ )
+
+ def test_create_tx_bp_adjustment(
+ self,
+ user,
+ product_user_wallet_no,
+ create_main_accounts,
+ caplog,
+ thl_lm,
+ lm,
+ currency,
+ session_manager,
+ wall_manager,
+ session_factory,
+ utc_hour_ago,
+ delete_ledger_db,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ s1 = session_factory(
+ user=user,
+ wall_count=2,
+ wall_req_cpis=[Decimal(1), Decimal(3)],
+ wall_statuses=[Status.COMPLETE, Status.COMPLETE],
+ started=utc_hour_ago,
+ )
+
+ w1: Wall = s1.wall_events[0]
+ w2: Wall = s1.wall_events[1]
+
+ thl_lm.create_tx_task_complete(wall=w1, user=user, created=w1.started)
+ thl_lm.create_tx_task_complete(wall=w2, user=user, created=w2.started)
+
+ status, status_code_1 = s1.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = s1.determine_payments()
+ session_manager.finish_with_status(
+ session=s1,
+ status=status,
+ status_code_1=status_code_1,
+ finished=utc_hour_ago + timedelta(minutes=10),
+ payout=bp_pay,
+ user_payout=user_pay,
+ )
+ thl_lm.create_tx_bp_payment(session=s1, created=w1.started)
+ revenue = thl_lm.get_account_task_complete_revenue()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(product=user.product)
+ bp_commission_account = thl_lm.get_account_or_create_bp_commission(
+ product=user.product
+ )
+ assert 380 == thl_lm.get_account_balance(account=bp_wallet_account)
+ assert 0 == thl_lm.get_account_balance(account=revenue)
+ assert 20 == thl_lm.get_account_balance(account=bp_commission_account)
+ thl_lm.check_ledger_balanced()
+
+ # This should do nothing (since we haven't adjusted any wall events)
+ s1.adjust_status()
+ with caplog.at_level(logging.INFO):
+ thl_lm.create_tx_bp_adjustment(session=s1)
+
+ assert (
+ "create_transaction_bp_adjustment. No transactions needed." in caplog.text
+ )
+
+ # self.assertEqual(380, ledger_manager.get_account_balance(bp_wallet_account))
+ # self.assertEqual(0, ledger_manager.get_account_balance(revenue))
+ # self.assertEqual(20, ledger_manager.get_account_balance(bp_commission_account))
+ # self.assertTrue(ledger_manager.check_ledger_balanced())
+
+ # recon $1 survey.
+ wall_manager.adjust_status(
+ wall=w1,
+ adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL,
+ adjusted_cpi=Decimal(0),
+ adjusted_timestamp=utc_hour_ago + timedelta(hours=1),
+ )
+ thl_lm.create_tx_task_adjustment(wall=w1, user=user)
+ # -$1.00 b/c the MP took the $1 back, but we haven't yet taken the BP payment back
+ assert -100 == thl_lm.get_account_balance(revenue)
+ s1.adjust_status()
+ thl_lm.create_tx_bp_adjustment(session=s1)
+
+ with caplog.at_level(logging.INFO):
+ thl_lm.create_tx_bp_adjustment(session=s1)
+ assert (
+ "create_transaction_bp_adjustment. No transactions needed." in caplog.text
+ )
+
+ assert 380 - 95 == thl_lm.get_account_balance(bp_wallet_account)
+ assert 0 == thl_lm.get_account_balance(revenue)
+ assert 20 - 5 == thl_lm.get_account_balance(bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
+
+ # unrecon the $1 survey
+ wall_manager.adjust_status(
+ wall=w1,
+ adjusted_status=None,
+ adjusted_cpi=None,
+ adjusted_timestamp=utc_hour_ago + timedelta(minutes=45),
+ )
+ thl_lm.create_tx_task_adjustment(
+ wall=w1,
+ user=user,
+ created=utc_hour_ago + timedelta(minutes=45),
+ )
+ new_status, new_payout, new_user_payout = s1.determine_new_status_and_payouts()
+ s1.adjust_status()
+ thl_lm.create_tx_bp_adjustment(session=s1)
+ assert 380 == thl_lm.get_account_balance(bp_wallet_account)
+ assert 0 == thl_lm.get_account_balance(revenue)
+ assert 20, thl_lm.get_account_balance(bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
+
+ def test_create_tx_bp_adjustment_small(
+ self,
+ user_factory,
+ product_user_wallet_no,
+ create_main_accounts,
+ delete_ledger_db,
+ thl_lm,
+ lm,
+ utc_hour_ago,
+ currency,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ # This failed when I didn't check that `change_commission` > 0 in
+ # create_transaction_bp_adjustment
+ user: User = user_factory(product=product_user_wallet_no)
+
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.DYNATA,
+ req_survey_id="xxx",
+ req_cpi=Decimal("0.10"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=utc_hour_ago,
+ finished=utc_hour_ago + timedelta(seconds=1),
+ )
+
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall1, user=user, created=wall1.started
+ )
+ assert isinstance(tx, LedgerTransaction)
+
+ session = Session(started=wall1.started, user=user, wall_events=[wall1])
+ status, status_code_1 = session.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = session.determine_payments()
+ session.update(
+ **{
+ "status": status,
+ "status_code_1": status_code_1,
+ "finished": utc_hour_ago + timedelta(minutes=10),
+ "payout": bp_pay,
+ "user_payout": user_pay,
+ }
+ )
+ thl_lm.create_tx_bp_payment(session, created=wall1.started)
+
+ wall1.update(
+ adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL,
+ adjusted_cpi=0,
+ adjusted_timestamp=utc_hour_ago + timedelta(hours=1),
+ )
+ thl_lm.create_tx_task_adjustment(wall1, user)
+ session.adjust_status()
+ thl_lm.create_tx_bp_adjustment(session)
+
+ def test_create_tx_bp_adjustment_abandon(
+ self,
+ user_factory,
+ product_user_wallet_no,
+ delete_ledger_db,
+ session_factory,
+ create_main_accounts,
+ caplog,
+ thl_lm,
+ lm,
+ currency,
+ utc_hour_ago,
+ session_manager,
+ wall_manager,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+ user: User = user_factory(product=product_user_wallet_no)
+ s1: Session = session_factory(
+ user=user, final_status=Status.ABANDON, wall_req_cpi=Decimal(1)
+ )
+ w1 = s1.wall_events[-1]
+
+ # Adjust to complete.
+ wall_manager.adjust_status(
+ wall=w1,
+ adjusted_status=WallAdjustedStatus.ADJUSTED_TO_COMPLETE,
+ adjusted_cpi=w1.cpi,
+ adjusted_timestamp=utc_hour_ago + timedelta(hours=1),
+ )
+ thl_lm.create_tx_task_adjustment(wall=w1, user=user)
+ s1.adjust_status()
+ thl_lm.create_tx_bp_adjustment(session=s1)
+ # And then adjust it back (it was abandon before, but now it should be
+ # fail (?) or back to abandon?)
+ wall_manager.adjust_status(
+ wall=w1,
+ adjusted_status=None,
+ adjusted_cpi=None,
+ adjusted_timestamp=utc_hour_ago + timedelta(hours=1),
+ )
+ thl_lm.create_tx_task_adjustment(wall=w1, user=user)
+ s1.adjust_status()
+ thl_lm.create_tx_bp_adjustment(session=s1)
+
+ revenue = thl_lm.get_account_task_complete_revenue()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(product=user.product)
+ bp_commission_account = thl_lm.get_account_or_create_bp_commission(
+ product=user.product
+ )
+ assert 0 == thl_lm.get_account_balance(bp_wallet_account)
+ assert 0 == thl_lm.get_account_balance(revenue)
+ assert 0 == thl_lm.get_account_balance(bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
+
+ # This should do nothing
+ s1.adjust_status()
+ with caplog.at_level(logging.INFO):
+ thl_lm.create_tx_bp_adjustment(session=s1)
+ assert "No transactions needed" in caplog.text
+
+ # Now back to complete again
+ wall_manager.adjust_status(
+ wall=w1,
+ adjusted_status=WallAdjustedStatus.ADJUSTED_TO_COMPLETE,
+ adjusted_cpi=w1.cpi,
+ adjusted_timestamp=utc_hour_ago + timedelta(hours=1),
+ )
+ s1.adjust_status()
+ thl_lm.create_tx_bp_adjustment(session=s1)
+ assert 95 == thl_lm.get_account_balance(bp_wallet_account)
+
+ def test_create_tx_bp_adjustment_user_wallet(
+ self,
+ user_factory,
+ product_user_wallet_yes,
+ create_main_accounts,
+ delete_ledger_db,
+ caplog,
+ thl_lm,
+ lm,
+ currency,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ now = datetime.now(timezone.utc) - timedelta(days=1)
+ user: User = user_factory(product=product_user_wallet_yes)
+
+ # Create 2 Wall completes and create the respective transaction for
+ # them. We then create a 3rd wall event which is a failure but we
+ # do NOT create a transaction for it
+
+ wall3 = Wall(
+ user_id=8,
+ source=Source.CINT,
+ req_survey_id="zzz",
+ req_cpi=Decimal("2.00"),
+ session_id=1,
+ status=Status.FAIL,
+ status_code_1=StatusCode1.BUYER_FAIL,
+ started=now,
+ finished=now + timedelta(minutes=1),
+ )
+
+ now_w1 = now + timedelta(minutes=1, milliseconds=1)
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.DYNATA,
+ req_survey_id="xxx",
+ req_cpi=Decimal("1.00"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=now_w1,
+ finished=now_w1 + timedelta(minutes=1),
+ )
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall1, user=user, created=wall1.started
+ )
+ assert isinstance(tx, LedgerTransaction)
+
+ now_w2 = now + timedelta(minutes=2, milliseconds=1)
+ wall2 = Wall(
+ user_id=user.user_id,
+ source=Source.FULL_CIRCLE,
+ req_survey_id="yyy",
+ req_cpi=Decimal("3.00"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=now_w2,
+ finished=now_w2 + timedelta(minutes=1),
+ )
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall2, user=user, created=wall2.started
+ )
+ assert isinstance(tx, LedgerTransaction)
+
+ # It doesn't matter what order these wall events go in as because
+ # we have a pydantic valiator that sorts them
+ wall_events = [wall3, wall1, wall2]
+ # shuffle(wall_events)
+ session = Session(started=wall1.started, user=user, wall_events=wall_events)
+ status, status_code_1 = session.determine_session_status()
+ assert status == Status.COMPLETE
+ assert status_code_1 == StatusCode1.COMPLETE
+
+ thl_net, commission_amount, bp_pay, user_pay = session.determine_payments()
+ assert thl_net == Decimal("4.00")
+ assert commission_amount == Decimal("0.20")
+ assert bp_pay == Decimal("3.80")
+ assert user_pay == Decimal("1.52")
+
+ session.update(
+ **{
+ "status": status,
+ "status_code_1": status_code_1,
+ "finished": now + timedelta(minutes=10),
+ "payout": bp_pay,
+ "user_payout": user_pay,
+ }
+ )
+
+ tx = thl_lm.create_tx_bp_adjustment(session=session, created=wall1.started)
+ assert isinstance(tx, LedgerTransaction)
+
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(product=user.product)
+ assert 228 == thl_lm.get_account_balance(account=bp_wallet_account)
+
+ user_account = thl_lm.get_account_or_create_user_wallet(user=user)
+ assert 152 == thl_lm.get_account_balance(account=user_account)
+
+ revenue = thl_lm.get_account_task_complete_revenue()
+ assert 0 == thl_lm.get_account_balance(account=revenue)
+
+ bp_commission_account = thl_lm.get_account_or_create_bp_commission(
+ product=user.product
+ )
+ assert 20 == thl_lm.get_account_balance(account=bp_commission_account)
+
+ # the total (4.00) = 2.28 + 1.52 + .20
+ assert thl_lm.check_ledger_balanced()
+
+ # This should do nothing (since we haven't adjusted any wall events)
+ session.adjust_status()
+ print(
+ session.get_status_after_adjustment(),
+ session.get_payout_after_adjustment(),
+ session.get_user_payout_after_adjustment(),
+ )
+ with caplog.at_level(logging.INFO):
+ thl_lm.create_tx_bp_adjustment(session)
+ assert (
+ "create_transaction_bp_adjustment. No transactions needed." in caplog.text
+ )
+
+ # recon $1 survey.
+ wall1.update(
+ adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL,
+ adjusted_cpi=0,
+ adjusted_timestamp=now + timedelta(hours=1),
+ )
+ thl_lm.create_tx_task_adjustment(wall1, user)
+ # -$1.00 b/c the MP took the $1 back, but we haven't yet taken the BP payment back
+ assert -100 == thl_lm.get_account_balance(revenue)
+ session.adjust_status()
+ print(
+ session.get_status_after_adjustment(),
+ session.get_payout_after_adjustment(),
+ session.get_user_payout_after_adjustment(),
+ )
+ thl_lm.create_tx_bp_adjustment(session)
+
+ # running this twice b/c it should do nothing the 2nd time
+ print(
+ session.get_status_after_adjustment(),
+ session.get_payout_after_adjustment(),
+ session.get_user_payout_after_adjustment(),
+ )
+ with caplog.at_level(logging.INFO):
+ thl_lm.create_tx_bp_adjustment(session)
+ assert (
+ "create_transaction_bp_adjustment. No transactions needed." in caplog.text
+ )
+
+ assert 228 - 57 == thl_lm.get_account_balance(bp_wallet_account)
+ assert 152 - 38 == thl_lm.get_account_balance(user_account)
+ assert 0 == thl_lm.get_account_balance(revenue)
+ assert 20 - 5 == thl_lm.get_account_balance(bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
+
+ # unrecon the $1 survey
+ wall1.update(
+ adjusted_status=None,
+ adjusted_cpi=None,
+ adjusted_timestamp=now + timedelta(hours=2),
+ )
+ tx = thl_lm.create_tx_task_adjustment(wall=wall1, user=user)
+ assert isinstance(tx, LedgerTransaction)
+
+ new_status, new_payout, new_user_payout = (
+ session.determine_new_status_and_payouts()
+ )
+ print(new_status, new_payout, new_user_payout, session.adjusted_payout)
+ session.adjust_status()
+ print(
+ session.get_status_after_adjustment(),
+ session.get_payout_after_adjustment(),
+ session.get_user_payout_after_adjustment(),
+ )
+ thl_lm.create_tx_bp_adjustment(session)
+
+ assert 228 - 57 + 57 == thl_lm.get_account_balance(bp_wallet_account)
+ assert 152 - 38 + 38 == thl_lm.get_account_balance(user_account)
+ assert 0 == thl_lm.get_account_balance(revenue)
+ assert 20 - 5 + 5 == thl_lm.get_account_balance(bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
+
+ # make the $2 failure into a complete also
+ wall3.update(
+ adjusted_status=WallAdjustedStatus.ADJUSTED_TO_COMPLETE,
+ adjusted_cpi=wall3.cpi,
+ adjusted_timestamp=now + timedelta(hours=2),
+ )
+ thl_lm.create_tx_task_adjustment(wall3, user)
+ new_status, new_payout, new_user_payout = (
+ session.determine_new_status_and_payouts()
+ )
+ print(new_status, new_payout, new_user_payout, session.adjusted_payout)
+ session.adjust_status()
+ print(
+ session.get_status_after_adjustment(),
+ session.get_payout_after_adjustment(),
+ session.get_user_payout_after_adjustment(),
+ )
+ thl_lm.create_tx_bp_adjustment(session)
+ assert 228 - 57 + 57 + 114 == thl_lm.get_account_balance(bp_wallet_account)
+ assert 152 - 38 + 38 + 76 == thl_lm.get_account_balance(user_account)
+ assert 0 == thl_lm.get_account_balance(revenue)
+ assert 20 - 5 + 5 + 10 == thl_lm.get_account_balance(bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
+
+ def test_create_transaction_bp_adjustment_cpi_adjustment(
+ self,
+ user_factory,
+ product_user_wallet_no,
+ create_main_accounts,
+ delete_ledger_db,
+ caplog,
+ thl_lm,
+ lm,
+ utc_hour_ago,
+ currency,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+ user: User = user_factory(product=product_user_wallet_no)
+
+ wall1 = Wall(
+ user_id=user.user_id,
+ source=Source.DYNATA,
+ req_survey_id="xxx",
+ req_cpi=Decimal("1.00"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=utc_hour_ago,
+ finished=utc_hour_ago + timedelta(seconds=1),
+ )
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall1, user=user, created=wall1.started
+ )
+ assert isinstance(tx, LedgerTransaction)
+
+ wall2 = Wall(
+ user_id=user.user_id,
+ source=Source.FULL_CIRCLE,
+ req_survey_id="yyy",
+ req_cpi=Decimal("3.00"),
+ session_id=1,
+ status=Status.COMPLETE,
+ status_code_1=StatusCode1.COMPLETE,
+ started=utc_hour_ago,
+ finished=utc_hour_ago + timedelta(seconds=1),
+ )
+ tx = thl_lm.create_tx_task_complete(
+ wall=wall2, user=user, created=wall2.started
+ )
+ assert isinstance(tx, LedgerTransaction)
+
+ session = Session(started=wall1.started, user=user, wall_events=[wall1, wall2])
+ status, status_code_1 = session.determine_session_status()
+ thl_net, commission_amount, bp_pay, user_pay = session.determine_payments()
+ session.update(
+ **{
+ "status": status,
+ "status_code_1": status_code_1,
+ "finished": utc_hour_ago + timedelta(minutes=10),
+ "payout": bp_pay,
+ "user_payout": user_pay,
+ }
+ )
+ thl_lm.create_tx_bp_payment(session, created=wall1.started)
+
+ revenue = thl_lm.get_account_task_complete_revenue()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(user.product)
+ bp_commission_account = thl_lm.get_account_or_create_bp_commission(user.product)
+ assert 380 == thl_lm.get_account_balance(bp_wallet_account)
+ assert 0 == thl_lm.get_account_balance(revenue)
+ assert 20 == thl_lm.get_account_balance(bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
+
+ # cpi adjustment $1 -> $.60.
+ wall1.update(
+ adjusted_status=WallAdjustedStatus.CPI_ADJUSTMENT,
+ adjusted_cpi=Decimal("0.60"),
+ adjusted_timestamp=utc_hour_ago + timedelta(minutes=30),
+ )
+ thl_lm.create_tx_task_adjustment(wall1, user)
+
+ # -$0.40 b/c the MP took $0.40 back, but we haven't yet taken the BP payment back
+ assert -40 == thl_lm.get_account_balance(revenue)
+ session.adjust_status()
+ print(
+ session.get_status_after_adjustment(),
+ session.get_payout_after_adjustment(),
+ session.get_user_payout_after_adjustment(),
+ )
+ thl_lm.create_tx_bp_adjustment(session)
+
+ # running this twice b/c it should do nothing the 2nd time
+ print(
+ session.get_status_after_adjustment(),
+ session.get_payout_after_adjustment(),
+ session.get_user_payout_after_adjustment(),
+ )
+ with caplog.at_level(logging.INFO):
+ thl_lm.create_tx_bp_adjustment(session)
+ assert "create_transaction_bp_adjustment." in caplog.text
+ assert "No transactions needed." in caplog.text
+
+ assert 380 - 38 == thl_lm.get_account_balance(bp_wallet_account)
+ assert 0 == thl_lm.get_account_balance(revenue)
+ assert 20 - 2 == thl_lm.get_account_balance(bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
+
+ # adjust it to failure
+ wall1.update(
+ adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL,
+ adjusted_cpi=0,
+ adjusted_timestamp=utc_hour_ago + timedelta(minutes=45),
+ )
+ thl_lm.create_tx_task_adjustment(wall1, user)
+ session.adjust_status()
+ thl_lm.create_tx_bp_adjustment(session)
+ assert 300 - (300 * 0.05) == thl_lm.get_account_balance(bp_wallet_account)
+ assert 0 == thl_lm.get_account_balance(revenue)
+ assert 300 * 0.05 == thl_lm.get_account_balance(bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
+
+ # and then back to cpi adj again, but this time for more than the orig amount
+ wall1.update(
+ adjusted_status=WallAdjustedStatus.CPI_ADJUSTMENT,
+ adjusted_cpi=Decimal("2.00"),
+ adjusted_timestamp=utc_hour_ago + timedelta(minutes=45),
+ )
+ thl_lm.create_tx_task_adjustment(wall1, user)
+ session.adjust_status()
+ thl_lm.create_tx_bp_adjustment(session)
+ assert 500 - (500 * 0.05) == thl_lm.get_account_balance(bp_wallet_account)
+ assert 0 == thl_lm.get_account_balance(revenue)
+ assert 500 * 0.05 == thl_lm.get_account_balance(bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
+
+ # And adjust again
+ wall1.update(
+ adjusted_status=WallAdjustedStatus.CPI_ADJUSTMENT,
+ adjusted_cpi=Decimal("3.00"),
+ adjusted_timestamp=utc_hour_ago + timedelta(minutes=45),
+ )
+ thl_lm.create_tx_task_adjustment(wall=wall1, user=user)
+ session.adjust_status()
+ thl_lm.create_tx_bp_adjustment(session=session)
+ assert 600 - (600 * 0.05) == thl_lm.get_account_balance(
+ account=bp_wallet_account
+ )
+ assert 0 == thl_lm.get_account_balance(account=revenue)
+ assert 600 * 0.05 == thl_lm.get_account_balance(account=bp_commission_account)
+ assert thl_lm.check_ledger_balanced()
diff --git a/tests/managers/thl/test_ledger/test_thl_lm_tx__user_payouts.py b/tests/managers/thl/test_ledger/test_thl_lm_tx__user_payouts.py
new file mode 100644
index 0000000..1e7146a
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_thl_lm_tx__user_payouts.py
@@ -0,0 +1,505 @@
+import logging
+from datetime import datetime, timezone, timedelta
+from decimal import Decimal
+from uuid import uuid4
+
+import pytest
+
+from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerTransactionFlagAlreadyExistsError,
+ LedgerTransactionConditionFailedError,
+)
+from generalresearch.models.thl.user import User
+from generalresearch.models.thl.wallet import PayoutType
+from generalresearch.models.thl.payout import UserPayoutEvent
+from test_utils.managers.ledger.conftest import create_main_accounts
+
+
+class TestLedgerManagerAMT:
+
+ def test_create_transaction_amt_ass_request(
+ self,
+ user_factory,
+ product_amt_true,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ delete_ledger_db,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+ user: User = user_factory(product=product_amt_true)
+
+ # debit_account_uuid nothing checks they match the ledger ... todo?
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.AMT_HIT,
+ amount=5,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+ flag_key = f"test:user_payout:{pe.uuid}:request"
+ flag_name = f"ledger-manager:transaction_flag:{flag_key}"
+ lm.redis_client.delete(flag_name)
+
+ # User has $0 in their wallet. They are allowed amt_assignment payouts until -$1.00
+ thl_lm.create_tx_user_payout_request(user=user, payout_event=pe)
+ with pytest.raises(expected_exception=LedgerTransactionFlagAlreadyExistsError):
+ thl_lm.create_tx_user_payout_request(
+ user=user, payout_event=pe, skip_flag_check=False
+ )
+ with pytest.raises(expected_exception=LedgerTransactionConditionFailedError):
+ thl_lm.create_tx_user_payout_request(
+ user=user, payout_event=pe, skip_flag_check=True
+ )
+ pe2 = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.AMT_HIT,
+ amount=96,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+
+ flag_key = f"test:user_payout:{pe2.uuid}:request"
+ flag_name = f"ledger-manager:transaction_flag:{flag_key}"
+ lm.redis_client.delete(flag_name)
+ # 96 cents would put them over the -$1.00 limit
+ with pytest.raises(expected_exception=LedgerTransactionConditionFailedError):
+ thl_lm.create_tx_user_payout_request(user, payout_event=pe2)
+
+ # But they could do 0.95 cents
+ pe2.amount = 95
+ thl_lm.create_tx_user_payout_request(
+ user, payout_event=pe2, skip_flag_check=True
+ )
+
+ cash = thl_lm.get_account_cash()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(user.product)
+ bp_pending_account = thl_lm.get_or_create_bp_pending_payout_account(
+ product=user.product
+ )
+ user_wallet_account = thl_lm.get_account_or_create_user_wallet(user=user)
+
+ assert 0 == lm.get_account_balance(account=bp_wallet_account)
+ assert 0 == lm.get_account_balance(account=cash)
+ assert 100 == lm.get_account_balance(account=bp_pending_account)
+ assert -100 == lm.get_account_balance(account=user_wallet_account)
+ assert thl_lm.check_ledger_balanced()
+ assert -5 == thl_lm.get_account_filtered_balance(
+ account=user_wallet_account,
+ metadata_key="payoutevent",
+ metadata_value=pe.uuid,
+ )
+
+ assert -95 == thl_lm.get_account_filtered_balance(
+ account=user_wallet_account,
+ metadata_key="payoutevent",
+ metadata_value=pe2.uuid,
+ )
+
+ def test_create_transaction_amt_ass_complete(
+ self,
+ user_factory,
+ product_amt_true,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ delete_ledger_db,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+ user: User = user_factory(product=product_amt_true)
+
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.AMT_HIT,
+ amount=5,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+ flag = f"ledger-manager:transaction_flag:test:user_payout:{pe.uuid}:request"
+ lm.redis_client.delete(flag)
+ flag = f"ledger-manager:transaction_flag:test:user_payout:{pe.uuid}:complete"
+ lm.redis_client.delete(flag)
+
+ # User has $0 in their wallet. They are allowed amt_assignment payouts until -$1.00
+ thl_lm.create_tx_user_payout_request(user, payout_event=pe)
+ thl_lm.create_tx_user_payout_complete(user, payout_event=pe)
+
+ cash = thl_lm.get_account_cash()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(user.product)
+ bp_pending_account = thl_lm.get_or_create_bp_pending_payout_account(
+ user.product
+ )
+ bp_amt_expense_account = thl_lm.get_account_or_create_bp_expense(
+ user.product, expense_name="amt"
+ )
+ user_wallet_account = thl_lm.get_account_or_create_user_wallet(user)
+
+ # BP wallet pays the 1cent fee
+ assert -1 == thl_lm.get_account_balance(bp_wallet_account)
+ assert -5 == thl_lm.get_account_balance(cash)
+ assert -1 == thl_lm.get_account_balance(bp_amt_expense_account)
+ assert 0 == thl_lm.get_account_balance(bp_pending_account)
+ assert -5 == lm.get_account_balance(user_wallet_account)
+ assert thl_lm.check_ledger_balanced()
+
+ def test_create_transaction_amt_bonus(
+ self,
+ user_factory,
+ product_amt_true,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ delete_ledger_db,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ user: User = user_factory(product=product_amt_true)
+
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.AMT_BONUS,
+ amount=34,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+ flag = f"ledger-manager:transaction_flag:test:user_payout:{pe.uuid}:request"
+ lm.redis_client.delete(flag)
+ flag = f"ledger-manager:transaction_flag:test:user_payout:{pe.uuid}:complete"
+ lm.redis_client.delete(flag)
+
+ with pytest.raises(expected_exception=LedgerTransactionConditionFailedError):
+ # User has $0 in their wallet. No amt bonus allowed
+ thl_lm.create_tx_user_payout_request(user, payout_event=pe)
+
+ thl_lm.create_tx_user_bonus(
+ user,
+ amount=Decimal(5),
+ ref_uuid="e703830dec124f17abed2d697d8d7701",
+ description="Bribe",
+ skip_flag_check=True,
+ )
+ pe.amount = 101
+ thl_lm.create_tx_user_payout_request(
+ user, payout_event=pe, skip_flag_check=False
+ )
+ thl_lm.create_tx_user_payout_complete(
+ user, payout_event=pe, skip_flag_check=False
+ )
+ with pytest.raises(expected_exception=LedgerTransactionFlagAlreadyExistsError):
+ # duplicate, even if amount changed
+ pe.amount = 200
+ thl_lm.create_tx_user_payout_complete(
+ user, payout_event=pe, skip_flag_check=False
+ )
+ with pytest.raises(expected_exception=LedgerTransactionConditionFailedError):
+ # duplicate
+ thl_lm.create_tx_user_payout_complete(
+ user, payout_event=pe, skip_flag_check=True
+ )
+ pe.uuid = "533364150de4451198e5774e221a2acb"
+ pe.amount = 9900
+ with pytest.raises(expected_exception=ValueError):
+ # Trying to complete payout with no pending tx
+ thl_lm.create_tx_user_payout_complete(
+ user, payout_event=pe, skip_flag_check=True
+ )
+ with pytest.raises(expected_exception=LedgerTransactionConditionFailedError):
+ # trying to payout $99 with only a $5 balance
+ thl_lm.create_tx_user_payout_request(
+ user, payout_event=pe, skip_flag_check=True
+ )
+
+ cash = thl_lm.get_account_cash()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(user.product)
+ bp_pending_account = thl_lm.get_or_create_bp_pending_payout_account(
+ user.product
+ )
+ bp_amt_expense_account = thl_lm.get_account_or_create_bp_expense(
+ user.product, expense_name="amt"
+ )
+ user_wallet_account = thl_lm.get_account_or_create_user_wallet(user)
+ assert -500 + round(-101 * 0.20) == thl_lm.get_account_balance(
+ bp_wallet_account
+ )
+ assert -101 == lm.get_account_balance(cash)
+ assert -20 == lm.get_account_balance(bp_amt_expense_account)
+ assert 0 == lm.get_account_balance(bp_pending_account)
+ assert 500 - 101 == lm.get_account_balance(user_wallet_account)
+ assert lm.check_ledger_balanced() is True
+
+ def test_create_transaction_amt_bonus_cancel(
+ self,
+ user_factory,
+ product_amt_true,
+ create_main_accounts,
+ caplog,
+ thl_lm,
+ lm,
+ delete_ledger_db,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ now = datetime.now(timezone.utc) - timedelta(hours=1)
+ user: User = user_factory(product=product_amt_true)
+
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.AMT_BONUS,
+ amount=101,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+
+ thl_lm.create_tx_user_bonus(
+ user,
+ amount=Decimal(5),
+ ref_uuid="c44f4da2db1d421ebc6a5e5241ca4ce6",
+ description="Bribe",
+ skip_flag_check=True,
+ )
+ thl_lm.create_tx_user_payout_request(
+ user, payout_event=pe, skip_flag_check=True
+ )
+ thl_lm.create_tx_user_payout_cancelled(
+ user, payout_event=pe, skip_flag_check=True
+ )
+ with pytest.raises(expected_exception=LedgerTransactionConditionFailedError):
+ with caplog.at_level(logging.WARNING):
+ thl_lm.create_tx_user_payout_complete(
+ user, payout_event=pe, skip_flag_check=True
+ )
+ assert "trying to complete payout that was already cancelled" in caplog.text
+
+ cash = thl_lm.get_account_cash()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(user.product)
+ bp_pending_account = thl_lm.get_or_create_bp_pending_payout_account(
+ user.product
+ )
+ bp_amt_expense_account = thl_lm.get_account_or_create_bp_expense(
+ user.product, expense_name="amt"
+ )
+ user_wallet_account = thl_lm.get_account_or_create_user_wallet(user)
+ assert -500 == thl_lm.get_account_balance(account=bp_wallet_account)
+ assert 0 == thl_lm.get_account_balance(account=cash)
+ assert 0 == thl_lm.get_account_balance(account=bp_amt_expense_account)
+ assert 0 == thl_lm.get_account_balance(account=bp_pending_account)
+ assert 500 == thl_lm.get_account_balance(account=user_wallet_account)
+ assert thl_lm.check_ledger_balanced()
+
+ pe2 = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.AMT_BONUS,
+ amount=200,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+ thl_lm.create_tx_user_payout_request(
+ user, payout_event=pe2, skip_flag_check=True
+ )
+ thl_lm.create_tx_user_payout_complete(
+ user, payout_event=pe2, skip_flag_check=True
+ )
+ with pytest.raises(expected_exception=LedgerTransactionConditionFailedError):
+ with caplog.at_level(logging.WARNING):
+ thl_lm.create_tx_user_payout_cancelled(
+ user, payout_event=pe2, skip_flag_check=True
+ )
+ assert "trying to cancel payout that was already completed" in caplog.text
+
+
+class TestLedgerManagerTango:
+
+ def test_create_transaction_tango_request(
+ self,
+ user_factory,
+ product_amt_true,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ delete_ledger_db,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ user: User = user_factory(product=product_amt_true)
+
+ # debit_account_uuid nothing checks they match the ledger ... todo?
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.TANGO,
+ amount=500,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+ flag_key = f"test:user_payout:{pe.uuid}:request"
+ flag_name = f"ledger-manager:transaction_flag:{flag_key}"
+ lm.redis_client.delete(flag_name)
+ thl_lm.create_tx_user_bonus(
+ user,
+ amount=Decimal(6),
+ ref_uuid="e703830dec124f17abed2d697d8d7701",
+ description="Bribe",
+ skip_flag_check=True,
+ )
+ thl_lm.create_tx_user_payout_request(
+ user, payout_event=pe, skip_flag_check=True
+ )
+
+ cash = thl_lm.get_account_cash()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(user.product)
+ bp_pending_account = thl_lm.get_or_create_bp_pending_payout_account(
+ user.product
+ )
+ bp_tango_expense_account = thl_lm.get_account_or_create_bp_expense(
+ user.product, expense_name="tango"
+ )
+ user_wallet_account = thl_lm.get_account_or_create_user_wallet(user)
+ assert -600 == thl_lm.get_account_balance(bp_wallet_account)
+ assert 0 == thl_lm.get_account_balance(cash)
+ assert 0 == thl_lm.get_account_balance(bp_tango_expense_account)
+ assert 500 == thl_lm.get_account_balance(bp_pending_account)
+ assert 600 - 500 == thl_lm.get_account_balance(user_wallet_account)
+ assert thl_lm.check_ledger_balanced()
+
+ thl_lm.create_tx_user_payout_complete(
+ user, payout_event=pe, skip_flag_check=True
+ )
+ assert -600 - round(500 * 0.035) == thl_lm.get_account_balance(
+ bp_wallet_account
+ )
+ assert -500, thl_lm.get_account_balance(cash)
+ assert round(-500 * 0.035) == thl_lm.get_account_balance(
+ bp_tango_expense_account
+ )
+ assert 0 == lm.get_account_balance(bp_pending_account)
+ assert 100 == lm.get_account_balance(user_wallet_account)
+ assert lm.check_ledger_balanced()
+
+
+class TestLedgerManagerPaypal:
+
+ def test_create_transaction_paypal_request(
+ self,
+ user_factory,
+ product_amt_true,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ delete_ledger_db,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ now = datetime.now(tz=timezone.utc) - timedelta(hours=1)
+ user: User = user_factory(product=product_amt_true)
+
+ # debit_account_uuid nothing checks they match the ledger ... todo?
+ pe = UserPayoutEvent(
+ uuid=uuid4().hex,
+ payout_type=PayoutType.PAYPAL,
+ amount=500,
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ )
+ flag_key = f"test:user_payout:{pe.uuid}:request"
+ flag_name = f"ledger-manager:transaction_flag:{flag_key}"
+ lm.redis_client.delete(flag_name)
+ thl_lm.create_tx_user_bonus(
+ user=user,
+ amount=Decimal(6),
+ ref_uuid="e703830dec124f17abed2d697d8d7701",
+ description="Bribe",
+ skip_flag_check=True,
+ )
+
+ thl_lm.create_tx_user_payout_request(
+ user, payout_event=pe, skip_flag_check=True
+ )
+
+ cash = thl_lm.get_account_cash()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(user.product)
+ bp_pending_account = thl_lm.get_or_create_bp_pending_payout_account(
+ product=user.product
+ )
+ bp_paypal_expense_account = thl_lm.get_account_or_create_bp_expense(
+ product=user.product, expense_name="paypal"
+ )
+ user_wallet_account = thl_lm.get_account_or_create_user_wallet(user=user)
+ assert -600 == lm.get_account_balance(account=bp_wallet_account)
+ assert 0 == lm.get_account_balance(account=cash)
+ assert 0 == lm.get_account_balance(account=bp_paypal_expense_account)
+ assert 500 == lm.get_account_balance(account=bp_pending_account)
+ assert 600 - 500 == lm.get_account_balance(account=user_wallet_account)
+ assert thl_lm.check_ledger_balanced()
+
+ thl_lm.create_tx_user_payout_complete(
+ user=user, payout_event=pe, skip_flag_check=True, fee_amount=Decimal("0.50")
+ )
+ assert -600 - 50 == thl_lm.get_account_balance(bp_wallet_account)
+ assert -500 == thl_lm.get_account_balance(cash)
+ assert -50 == thl_lm.get_account_balance(bp_paypal_expense_account)
+ assert 0 == thl_lm.get_account_balance(bp_pending_account)
+ assert 100 == thl_lm.get_account_balance(user_wallet_account)
+ assert thl_lm.check_ledger_balanced()
+
+
+class TestLedgerManagerBonus:
+
+ def test_create_transaction_bonus(
+ self,
+ user_factory,
+ product_user_wallet_yes,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ delete_ledger_db,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ user: User = user_factory(product=product_user_wallet_yes)
+
+ thl_lm.create_tx_user_bonus(
+ user=user,
+ amount=Decimal(5),
+ ref_uuid="8d0aaf612462448a9ebdd57fab0fc660",
+ description="Bribe",
+ skip_flag_check=True,
+ )
+ cash = thl_lm.get_account_cash()
+ bp_wallet_account = thl_lm.get_account_or_create_bp_wallet(user.product)
+ bp_pending_account = thl_lm.get_or_create_bp_pending_payout_account(
+ product=user.product
+ )
+ bp_amt_expense_account = thl_lm.get_account_or_create_bp_expense(
+ user.product, expense_name="amt"
+ )
+ user_wallet_account = thl_lm.get_account_or_create_user_wallet(user=user)
+
+ assert -500 == lm.get_account_balance(account=bp_wallet_account)
+ assert 0 == lm.get_account_balance(account=cash)
+ assert 0 == lm.get_account_balance(account=bp_amt_expense_account)
+ assert 0 == lm.get_account_balance(account=bp_pending_account)
+ assert 500 == lm.get_account_balance(account=user_wallet_account)
+ assert thl_lm.check_ledger_balanced()
+
+ with pytest.raises(expected_exception=LedgerTransactionFlagAlreadyExistsError):
+ thl_lm.create_tx_user_bonus(
+ user=user,
+ amount=Decimal(5),
+ ref_uuid="8d0aaf612462448a9ebdd57fab0fc660",
+ description="Bribe",
+ skip_flag_check=False,
+ )
+ with pytest.raises(expected_exception=LedgerTransactionConditionFailedError):
+ thl_lm.create_tx_user_bonus(
+ user=user,
+ amount=Decimal(5),
+ ref_uuid="8d0aaf612462448a9ebdd57fab0fc660",
+ description="Bribe",
+ skip_flag_check=True,
+ )
diff --git a/tests/managers/thl/test_ledger/test_thl_pem.py b/tests/managers/thl/test_ledger/test_thl_pem.py
new file mode 100644
index 0000000..5fb9e7d
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_thl_pem.py
@@ -0,0 +1,251 @@
+import uuid
+from random import randint
+from uuid import uuid4, UUID
+
+import pytest
+
+from generalresearch.currency import USDCent
+from generalresearch.models.thl.definitions import PayoutStatus
+from generalresearch.models.thl.payout import BrokerageProductPayoutEvent
+from generalresearch.models.thl.product import Product
+from generalresearch.models.thl.wallet.cashout_method import (
+ CashoutRequestInfo,
+)
+
+
+class TestThlPayoutEventManager:
+
+ def test_get_by_uuid(self, brokerage_product_payout_event_manager, thl_lm):
+ """This validates that the method raises an exception if it
+ fails. There are plenty of other tests that use this method so
+ it seems silly to duplicate it here again
+ """
+
+ with pytest.raises(expected_exception=AssertionError) as excinfo:
+ brokerage_product_payout_event_manager.get_by_uuid(pe_uuid=uuid4().hex)
+ assert "expected 1 result, got 0" in str(excinfo.value)
+
+ def test_filter_by(
+ self,
+ product_factory,
+ usd_cent,
+ bp_payout_event_factory,
+ thl_lm,
+ brokerage_product_payout_event_manager,
+ ):
+ from generalresearch.models.thl.payout import UserPayoutEvent
+
+ N_PRODUCTS = randint(3, 10)
+ N_PAYOUT_EVENTS = randint(3, 10)
+ amounts = []
+ products = []
+
+ for x_idx in range(N_PRODUCTS):
+ product: Product = product_factory()
+ thl_lm.get_account_or_create_bp_wallet(product=product)
+ products.append(product)
+ brokerage_product_payout_event_manager.set_account_lookup_table(
+ thl_lm=thl_lm
+ )
+
+ for y_idx in range(N_PAYOUT_EVENTS):
+ pe = bp_payout_event_factory(product=product, usd_cent=usd_cent)
+ amounts.append(int(usd_cent))
+ assert isinstance(pe, BrokerageProductPayoutEvent)
+
+ # We just added Payout Events for Products, now go ahead and
+ # query for them
+ accounts = thl_lm.get_accounts_bp_wallet_for_products(
+ product_uuids=[i.uuid for i in products]
+ )
+ res = brokerage_product_payout_event_manager.filter_by(
+ debit_account_uuids=[i.uuid for i in accounts]
+ )
+
+ assert len(res) == (N_PRODUCTS * N_PAYOUT_EVENTS)
+ assert sum([i.amount for i in res]) == sum(amounts)
+
+ def test_get_bp_payout_events_for_product(
+ self,
+ product_factory,
+ usd_cent,
+ bp_payout_event_factory,
+ brokerage_product_payout_event_manager,
+ thl_lm,
+ ):
+ from generalresearch.models.thl.payout import UserPayoutEvent
+
+ N_PRODUCTS = randint(3, 10)
+ N_PAYOUT_EVENTS = randint(3, 10)
+ amounts = []
+ products = []
+
+ for x_idx in range(N_PRODUCTS):
+ product: Product = product_factory()
+ products.append(product)
+ thl_lm.get_account_or_create_bp_wallet(product=product)
+ brokerage_product_payout_event_manager.set_account_lookup_table(
+ thl_lm=thl_lm
+ )
+
+ for y_idx in range(N_PAYOUT_EVENTS):
+ pe = bp_payout_event_factory(product=product, usd_cent=usd_cent)
+ amounts.append(usd_cent)
+ assert isinstance(pe, BrokerageProductPayoutEvent)
+
+ # We just added 5 Payouts for a specific Product, now go
+ # ahead and query for them
+ res = brokerage_product_payout_event_manager.get_bp_bp_payout_events_for_products(
+ thl_ledger_manager=thl_lm, product_uuids=[product.id]
+ )
+
+ assert len(res) == N_PAYOUT_EVENTS
+
+ # Now that all the Payouts for all the Products have been added, go
+ # ahead and query for them
+ res = (
+ brokerage_product_payout_event_manager.get_bp_bp_payout_events_for_products(
+ thl_ledger_manager=thl_lm, product_uuids=[i.uuid for i in products]
+ )
+ )
+
+ assert len(res) == (N_PRODUCTS * N_PAYOUT_EVENTS)
+ assert sum([i.amount for i in res]) == sum(amounts)
+
+ @pytest.mark.skip
+ def test_get_payout_detail(self, user_payout_event_manager):
+ """This fails because the description coming back is None, but then
+ it tries to return a PayoutEvent which validates that the
+ description can't be None
+ """
+ from generalresearch.models.thl.payout import (
+ UserPayoutEvent,
+ PayoutType,
+ )
+
+ rand_amount = randint(a=99, b=999)
+
+ pe = user_payout_event_manager.create(
+ debit_account_uuid=uuid4().hex,
+ account_reference_type="str-type-random",
+ account_reference_uuid=uuid4().hex,
+ cashout_method_uuid=uuid4().hex,
+ description="Best payout !",
+ amount=rand_amount,
+ status=PayoutStatus.PENDING,
+ ext_ref_id="123",
+ payout_type=PayoutType.CASH_IN_MAIL,
+ request_data={"foo": 123},
+ order_data={},
+ )
+
+ res = user_payout_event_manager.get_payout_detail(pe_uuid=pe.uuid)
+ assert isinstance(res, CashoutRequestInfo)
+
+ # def test_filter_by(self):
+ # raise NotImplementedError
+
+ def test_create(self, user_payout_event_manager):
+ from generalresearch.models.thl.payout import UserPayoutEvent
+
+ # Confirm the creation method returns back an instance.
+ pe = user_payout_event_manager.create_dummy()
+ assert isinstance(pe, UserPayoutEvent)
+
+ # Now query the DB for that PayoutEvent to confirm it was actually
+ # saved.
+ res = user_payout_event_manager.get_by_uuid(pe_uuid=pe.uuid)
+ assert isinstance(res, UserPayoutEvent)
+ assert UUID(res.uuid)
+
+ # Confirm they're the same
+ # assert pe.model_dump_json() == res2.model_dump_json()
+ assert res.description is None
+
+ # def test_update(self):
+ # raise NotImplementedError
+
+ def test_create_bp_payout(
+ self,
+ product,
+ delete_ledger_db,
+ create_main_accounts,
+ thl_lm,
+ brokerage_product_payout_event_manager,
+ lm,
+ ):
+ from generalresearch.models.thl.payout import UserPayoutEvent
+
+ delete_ledger_db()
+ create_main_accounts()
+
+ account_bp_wallet = thl_lm.get_account_or_create_bp_wallet(product=product)
+ brokerage_product_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm)
+
+ rand_amount = randint(a=99, b=999)
+
+ # Save a Brokerage Product Payout, so we have something in the
+ # Payout Event table and the respective ledger TX and Entry rows for it
+ pe = brokerage_product_payout_event_manager.create_bp_payout_event(
+ thl_ledger_manager=thl_lm,
+ product=product,
+ amount=USDCent(rand_amount),
+ skip_wallet_balance_check=True,
+ skip_one_per_day_check=True,
+ )
+ assert isinstance(pe, BrokerageProductPayoutEvent)
+
+ # Now try to query for it!
+ res = thl_lm.get_tx_bp_payouts(account_uuids=[account_bp_wallet.uuid])
+ assert len(res) == 1
+ res = thl_lm.get_tx_bp_payouts(account_uuids=[uuid4().hex])
+ assert len(res) == 0
+
+ # Confirm it added to the users balance. The amount is negative because
+ # money was sent to the Brokerage Product, but they didn't have
+ # any activity that earned them money
+ bal = lm.get_account_balance(account=account_bp_wallet)
+ assert rand_amount == bal * -1
+
+
+class TestBPPayoutEvent:
+
+ def test_get_bp_bp_payout_events_for_products(
+ self,
+ product_factory,
+ bp_payout_event_factory,
+ usd_cent,
+ delete_ledger_db,
+ create_main_accounts,
+ brokerage_product_payout_event_manager,
+ thl_lm,
+ ):
+ delete_ledger_db()
+ create_main_accounts()
+
+ N_PAYOUT_EVENTS = randint(3, 10)
+ amounts = []
+
+ product: Product = product_factory()
+ thl_lm.get_account_or_create_bp_wallet(product=product)
+ brokerage_product_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm)
+
+ for y_idx in range(N_PAYOUT_EVENTS):
+ bp_payout_event_factory(product=product, usd_cent=usd_cent)
+ amounts.append(usd_cent)
+
+ # Fetch using the _bp_bp_ approach, so we have an
+ # array of BPPayoutEvents
+ bp_bp_res = (
+ brokerage_product_payout_event_manager.get_bp_bp_payout_events_for_products(
+ thl_ledger_manager=thl_lm, product_uuids=[product.uuid]
+ )
+ )
+ assert isinstance(bp_bp_res, list)
+ assert sum(amounts) == sum([i.amount for i in bp_bp_res])
+ for i in bp_bp_res:
+ assert isinstance(i, BrokerageProductPayoutEvent)
+ assert isinstance(i.amount, int)
+ assert isinstance(i.amount_usd, USDCent)
+ assert isinstance(i.amount_usd_str, str)
+ assert i.amount_usd_str[0] == "$"
diff --git a/tests/managers/thl/test_ledger/test_user_txs.py b/tests/managers/thl/test_ledger/test_user_txs.py
new file mode 100644
index 0000000..d81b244
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_user_txs.py
@@ -0,0 +1,288 @@
+from datetime import timedelta, datetime, timezone
+from decimal import Decimal
+from uuid import uuid4
+
+from generalresearch.managers.thl.payout import (
+ AMT_ASSIGNMENT_CASHOUT_METHOD,
+ AMT_BONUS_CASHOUT_METHOD,
+)
+from generalresearch.managers.thl.user_compensate import user_compensate
+from generalresearch.models.thl.definitions import (
+ Status,
+ WallAdjustedStatus,
+)
+from generalresearch.models.thl.ledger import (
+ UserLedgerTransactionTypesSummary,
+ UserLedgerTransactionTypeSummary,
+ TransactionType,
+)
+from generalresearch.models.thl.session import Session
+from generalresearch.models.thl.user import User
+from generalresearch.models.thl.wallet import PayoutType
+
+
+def test_user_txs(
+ user_factory,
+ product_amt_true,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ delete_ledger_db,
+ session_with_tx_factory,
+ adj_to_fail_with_tx_factory,
+ adj_to_complete_with_tx_factory,
+ session_factory,
+ user_payout_event_manager,
+ utc_now,
+):
+ delete_ledger_db()
+ create_main_accounts()
+
+ user: User = user_factory(product=product_amt_true)
+ account = thl_lm.get_account_or_create_user_wallet(user)
+ print(f"{account.uuid=}")
+
+ s: Session = session_with_tx_factory(user=user, wall_req_cpi=Decimal("1.00"))
+
+ bribe_uuid = user_compensate(
+ ledger_manager=thl_lm,
+ user=user,
+ amount_int=100,
+ )
+
+ pe = user_payout_event_manager.create(
+ uuid=uuid4().hex,
+ debit_account_uuid=account.uuid,
+ cashout_method_uuid=AMT_ASSIGNMENT_CASHOUT_METHOD,
+ amount=5,
+ created=utc_now,
+ payout_type=PayoutType.AMT_HIT,
+ request_data=dict(),
+ )
+ thl_lm.create_tx_user_payout_request(
+ user=user,
+ payout_event=pe,
+ )
+ pe = user_payout_event_manager.create(
+ uuid=uuid4().hex,
+ debit_account_uuid=account.uuid,
+ cashout_method_uuid=AMT_BONUS_CASHOUT_METHOD,
+ amount=127,
+ created=utc_now,
+ payout_type=PayoutType.AMT_BONUS,
+ request_data=dict(),
+ )
+ thl_lm.create_tx_user_payout_request(
+ user=user,
+ payout_event=pe,
+ )
+
+ wall = s.wall_events[-1]
+ adj_to_fail_with_tx_factory(session=s, created=wall.finished)
+
+ # And a fail -> complete adjustment
+ s_fail: Session = session_factory(
+ user=user,
+ wall_count=1,
+ final_status=Status.FAIL,
+ wall_req_cpi=Decimal("2.00"),
+ )
+ adj_to_complete_with_tx_factory(session=s_fail, created=utc_now)
+
+ # txs = thl_lm.get_tx_filtered_by_account(account.uuid)
+ # print(len(txs), txs)
+ txs = thl_lm.get_user_txs(user)
+ assert len(txs.transactions) == 6
+ assert txs.total == 6
+ assert txs.page == 1
+ assert txs.size == 50
+
+ # print(len(txs.transactions), txs)
+ d = txs.model_dump_json()
+ # print(d)
+
+ descriptions = {x.description for x in txs.transactions}
+ assert descriptions == {
+ "Compensation Bonus",
+ "HIT Bonus",
+ "HIT Reward",
+ "Task Adjustment",
+ "Task Complete",
+ }
+ amounts = {x.amount for x in txs.transactions}
+ assert amounts == {-127, 100, 38, -38, -5, 76}
+
+ assert txs.summary == UserLedgerTransactionTypesSummary(
+ bp_adjustment=UserLedgerTransactionTypeSummary(
+ entry_count=2, min_amount=-38, max_amount=76, total_amount=76 - 38
+ ),
+ bp_payment=UserLedgerTransactionTypeSummary(
+ entry_count=1, min_amount=38, max_amount=38, total_amount=38
+ ),
+ user_bonus=UserLedgerTransactionTypeSummary(
+ entry_count=1, min_amount=100, max_amount=100, total_amount=100
+ ),
+ user_payout_request=UserLedgerTransactionTypeSummary(
+ entry_count=2, min_amount=-127, max_amount=-5, total_amount=-132
+ ),
+ )
+ tx_adj_c = [
+ tx for tx in txs.transactions if tx.tx_type == TransactionType.BP_ADJUSTMENT
+ ]
+ assert sorted([tx.amount for tx in tx_adj_c]) == [-38, 76]
+
+
+def test_user_txs_pagination(
+ user_factory,
+ product_amt_true,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ delete_ledger_db,
+ session_with_tx_factory,
+ adj_to_fail_with_tx_factory,
+ user_payout_event_manager,
+ utc_now,
+):
+ delete_ledger_db()
+ create_main_accounts()
+
+ user: User = user_factory(product=product_amt_true)
+ account = thl_lm.get_account_or_create_user_wallet(user)
+ print(f"{account.uuid=}")
+
+ for _ in range(12):
+ user_compensate(
+ ledger_manager=thl_lm,
+ user=user,
+ amount_int=100,
+ skip_flag_check=True,
+ )
+
+ txs = thl_lm.get_user_txs(user, page=1, size=5)
+ assert len(txs.transactions) == 5
+ assert txs.total == 12
+ assert txs.page == 1
+ assert txs.size == 5
+ assert txs.summary.user_bonus.total_amount == 1200
+ assert txs.summary.user_bonus.entry_count == 12
+
+ # Skip to the 3rd page. We made 12, so there are 2 left
+ txs = thl_lm.get_user_txs(user, page=3, size=5)
+ assert len(txs.transactions) == 2
+ assert txs.total == 12
+ assert txs.page == 3
+ assert txs.summary.user_bonus.total_amount == 1200
+ assert txs.summary.user_bonus.entry_count == 12
+
+ # Should be empty, not fail
+ txs = thl_lm.get_user_txs(user, page=4, size=5)
+ assert len(txs.transactions) == 0
+ assert txs.total == 12
+ assert txs.page == 4
+ assert txs.summary.user_bonus.total_amount == 1200
+ assert txs.summary.user_bonus.entry_count == 12
+
+ # Test filtering. We should pull back only this one
+ now = datetime.now(tz=timezone.utc)
+ user_compensate(
+ ledger_manager=thl_lm,
+ user=user,
+ amount_int=100,
+ skip_flag_check=True,
+ )
+ txs = thl_lm.get_user_txs(user, page=1, size=5, time_start=now)
+ assert len(txs.transactions) == 1
+ assert txs.total == 1
+ assert txs.page == 1
+ # And the summary is restricted to this time range also!
+ assert txs.summary.user_bonus.total_amount == 100
+ assert txs.summary.user_bonus.entry_count == 1
+
+ # And filtering with 0 results
+ now = datetime.now(tz=timezone.utc)
+ txs = thl_lm.get_user_txs(user, page=1, size=5, time_start=now)
+ assert len(txs.transactions) == 0
+ assert txs.total == 0
+ assert txs.page == 1
+ assert txs.pages == 0
+ # And the summary is restricted to this time range also!
+ assert txs.summary.user_bonus.total_amount == None
+ assert txs.summary.user_bonus.entry_count == 0
+
+
+def test_user_txs_rolling_balance(
+ user_factory,
+ product_amt_true,
+ create_main_accounts,
+ thl_lm,
+ lm,
+ delete_ledger_db,
+ session_with_tx_factory,
+ adj_to_fail_with_tx_factory,
+ user_payout_event_manager,
+):
+ """
+ Creates 3 $1.00 bonuses (postive),
+ then 1 cashout (negative), $1.50
+ then 3 more $1.00 bonuses.
+ Note: pagination + rolling balance will BREAK if txs have
+ identical timestamps. In practice, they do not.
+ """
+ delete_ledger_db()
+ create_main_accounts()
+
+ user: User = user_factory(product=product_amt_true)
+ account = thl_lm.get_account_or_create_user_wallet(user)
+
+ for _ in range(3):
+ user_compensate(
+ ledger_manager=thl_lm,
+ user=user,
+ amount_int=100,
+ skip_flag_check=True,
+ )
+ pe = user_payout_event_manager.create(
+ uuid=uuid4().hex,
+ debit_account_uuid=account.uuid,
+ cashout_method_uuid=AMT_BONUS_CASHOUT_METHOD,
+ amount=150,
+ payout_type=PayoutType.AMT_BONUS,
+ request_data=dict(),
+ )
+ thl_lm.create_tx_user_payout_request(
+ user=user,
+ payout_event=pe,
+ )
+ for _ in range(3):
+ user_compensate(
+ ledger_manager=thl_lm,
+ user=user,
+ amount_int=100,
+ skip_flag_check=True,
+ )
+
+ txs = thl_lm.get_user_txs(user, page=1, size=10)
+ assert txs.transactions[0].balance_after == 100
+ assert txs.transactions[1].balance_after == 200
+ assert txs.transactions[2].balance_after == 300
+ assert txs.transactions[3].balance_after == 150
+ assert txs.transactions[4].balance_after == 250
+ assert txs.transactions[5].balance_after == 350
+ assert txs.transactions[6].balance_after == 450
+
+ # Ascending order, get 2nd page, make sure the balances include
+ # the previous txs. (will return last 3 txs)
+ txs = thl_lm.get_user_txs(user, page=2, size=4)
+ assert len(txs.transactions) == 3
+ assert txs.transactions[0].balance_after == 250
+ assert txs.transactions[1].balance_after == 350
+ assert txs.transactions[2].balance_after == 450
+
+ # Descending order, get 1st page. Will
+ # return most recent 3 txs in desc order
+ txs = thl_lm.get_user_txs(user, page=1, size=3, order_by="-created")
+ assert len(txs.transactions) == 3
+ assert txs.transactions[0].balance_after == 450
+ assert txs.transactions[1].balance_after == 350
+ assert txs.transactions[2].balance_after == 250
diff --git a/tests/managers/thl/test_ledger/test_wallet.py b/tests/managers/thl/test_ledger/test_wallet.py
new file mode 100644
index 0000000..a0abd7c
--- /dev/null
+++ b/tests/managers/thl/test_ledger/test_wallet.py
@@ -0,0 +1,78 @@
+from decimal import Decimal
+from uuid import uuid4
+
+import pytest
+
+from generalresearch.models.thl.product import (
+ UserWalletConfig,
+ PayoutConfig,
+ PayoutTransformation,
+ PayoutTransformationPercentArgs,
+)
+from generalresearch.models.thl.user import User
+
+
+@pytest.fixture()
+def schrute_product(product_manager):
+ return product_manager.create_dummy(
+ user_wallet_config=UserWalletConfig(enabled=True, amt=False),
+ payout_config=PayoutConfig(
+ payout_transformation=PayoutTransformation(
+ f="payout_transformation_percent",
+ kwargs=PayoutTransformationPercentArgs(pct=0.4),
+ ),
+ payout_format="{payout:,.0f} Schrute Bucks",
+ ),
+ )
+
+
+class TestGetUserWalletBalance:
+ def test_get_user_wallet_balance_non_managed(self, user, thl_lm):
+ with pytest.raises(
+ AssertionError,
+ match="Can't get wallet balance on non-managed account.",
+ ):
+ thl_lm.get_user_wallet_balance(user=user)
+
+ def test_get_user_wallet_balance_managed_0(
+ self, schrute_product, user_factory, thl_lm
+ ):
+ assert (
+ schrute_product.payout_config.payout_format == "{payout:,.0f} Schrute Bucks"
+ )
+ user: User = user_factory(schrute_product)
+ balance = thl_lm.get_user_wallet_balance(user=user)
+ assert balance == 0
+ balance_string = user.product.format_payout_format(Decimal(balance) / 100)
+ assert balance_string == "0 Schrute Bucks"
+ redeemable_balance = thl_lm.get_user_redeemable_wallet_balance(
+ user=user, user_wallet_balance=balance
+ )
+ assert redeemable_balance == 0
+ redeemable_balance_string = user.product.format_payout_format(
+ Decimal(redeemable_balance) / 100
+ )
+ assert redeemable_balance_string == "0 Schrute Bucks"
+
+ def test_get_user_wallet_balance_managed(
+ self, schrute_product, user_factory, thl_lm, session_with_tx_factory
+ ):
+ user: User = user_factory(schrute_product)
+ thl_lm.create_tx_user_bonus(
+ user=user,
+ amount=Decimal(1),
+ ref_uuid=uuid4().hex,
+ description="cheese",
+ )
+ session_with_tx_factory(user=user, wall_req_cpi=Decimal("1.23"))
+
+ # This product has a payout xform of 40% and commission of 5%
+ # 1.23 * 0.05 = 0.06 of commission
+ # 1.17 of payout * 0.40 = 0.47 of user pay and (1.17-0.47) 0.70 bp pay
+ balance = thl_lm.get_user_wallet_balance(user=user)
+ assert balance == 47 + 100 # plus the $1 bribe
+
+ redeemable_balance = thl_lm.get_user_redeemable_wallet_balance(
+ user=user, user_wallet_balance=balance
+ )
+ assert redeemable_balance == 20 + 100