diff options
| author | Max Nanis | 2026-03-06 16:49:46 -0500 |
|---|---|---|
| committer | Max Nanis | 2026-03-06 16:49:46 -0500 |
| commit | 91d040211a4ed6e4157896256a762d3854777b5e (patch) | |
| tree | cd95922ea4257dc8d3f4e4cbe8534474709a20dc /tests/models/thl/test_adjustments.py | |
| download | generalresearch-91d040211a4ed6e4157896256a762d3854777b5e.tar.gz generalresearch-91d040211a4ed6e4157896256a762d3854777b5e.zip | |
Initial commitv3.3.4
Diffstat (limited to 'tests/models/thl/test_adjustments.py')
| -rw-r--r-- | tests/models/thl/test_adjustments.py | 688 |
1 files changed, 688 insertions, 0 deletions
diff --git a/tests/models/thl/test_adjustments.py b/tests/models/thl/test_adjustments.py new file mode 100644 index 0000000..15d01d0 --- /dev/null +++ b/tests/models/thl/test_adjustments.py @@ -0,0 +1,688 @@ +from datetime import datetime, timezone, timedelta +from decimal import Decimal + +import pytest + +from generalresearch.models import Source +from generalresearch.models.thl.session import ( + Wall, + Status, + StatusCode1, + WallAdjustedStatus, + SessionAdjustedStatus, +) + +started1 = datetime(2023, 1, 1, tzinfo=timezone.utc) +started2 = datetime(2023, 1, 1, 0, 10, 0, tzinfo=timezone.utc) +finished1 = started1 + timedelta(minutes=10) +finished2 = started2 + timedelta(minutes=10) + +adj_ts = datetime(2023, 2, 2, tzinfo=timezone.utc) +adj_ts2 = datetime(2023, 2, 3, tzinfo=timezone.utc) +adj_ts3 = datetime(2023, 2, 4, tzinfo=timezone.utc) + + +class TestProductAdjustments: + + @pytest.mark.parametrize("payout", [".6", "1", "1.8", "2", "500.0000"]) + def test_determine_bp_payment_no_rounding(self, product_factory, payout): + p1 = product_factory(commission_pct=Decimal("0.05")) + res = p1.determine_bp_payment(thl_net=Decimal(payout)) + assert isinstance(res, Decimal) + assert res == Decimal(payout) * Decimal("0.95") + + @pytest.mark.parametrize("payout", [".01", ".05", ".5"]) + def test_determine_bp_payment_rounding(self, product_factory, payout): + p1 = product_factory(commission_pct=Decimal("0.05")) + res = p1.determine_bp_payment(thl_net=Decimal(payout)) + assert isinstance(res, Decimal) + assert res != Decimal(payout) * Decimal("0.95") + + +class TestSessionAdjustments: + + def test_status_complete(self, session_factory, user): + # Completed Session with 2 wall events + s1 = session_factory( + user=user, + wall_count=2, + wall_req_cpi=Decimal(1), + final_status=Status.COMPLETE, + started=started1, + ) + + # Confirm only the last Wall Event is a complete + assert not s1.wall_events[0].status == Status.COMPLETE + assert s1.wall_events[1].status == Status.COMPLETE + + # Confirm the Session is marked as finished and the simple brokerage + # payout calculation is correct. + status, status_code_1 = s1.determine_session_status() + assert status == Status.COMPLETE + assert status_code_1 == StatusCode1.COMPLETE + + +class TestAdjustments: + + def test_finish_with_status(self, session_factory, user, session_manager): + # Completed Session with 2 wall events + s1 = session_factory( + user=user, + wall_count=2, + wall_req_cpi=Decimal(1), + final_status=Status.COMPLETE, + started=started1, + ) + + status, status_code_1 = s1.determine_session_status() + payout = user.product.determine_bp_payment(Decimal(1)) + session_manager.finish_with_status( + session=s1, + status=status, + status_code_1=status_code_1, + payout=payout, + finished=finished2, + ) + + assert Decimal("0.95") == payout + + def test_never_adjusted(self, session_factory, user, session_manager): + s1 = session_factory( + user=user, + wall_count=5, + wall_req_cpi=Decimal(1), + final_status=Status.COMPLETE, + started=started1, + ) + + session_manager.finish_with_status( + session=s1, + status=Status.COMPLETE, + status_code_1=StatusCode1.COMPLETE, + payout=Decimal("0.95"), + finished=finished2, + ) + + # Confirm walls and Session are never adjusted in anyway + for w in s1.wall_events: + w: Wall + assert w.adjusted_status is None + assert w.adjusted_timestamp is None + assert w.adjusted_cpi is None + + assert s1.adjusted_status is None + assert s1.adjusted_payout is None + assert s1.adjusted_timestamp is None + + def test_adjustment_wall_values( + self, session_factory, user, session_manager, wall_manager + ): + # Completed Session with 2 wall events + s1 = session_factory( + user=user, + wall_count=5, + wall_req_cpi=Decimal(1), + final_status=Status.COMPLETE, + started=started1, + ) + + session_manager.finish_with_status( + session=s1, + status=Status.COMPLETE, + status_code_1=StatusCode1.COMPLETE, + payout=Decimal("0.95"), + finished=finished2, + ) + + # Change the last wall event to a Failure + w: Wall = s1.wall_events[-1] + wall_manager.adjust_status( + wall=w, + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL, + adjusted_timestamp=adj_ts, + ) + + # Original Session and Wall status is still the same, but the Adjusted + # values have changed + assert s1.status == Status.COMPLETE + assert s1.adjusted_status is None + assert s1.adjusted_timestamp is None + assert s1.adjusted_payout is None + assert s1.adjusted_user_payout is None + + assert w.status == Status.COMPLETE + assert w.status_code_1 == StatusCode1.COMPLETE + assert w.adjusted_status == WallAdjustedStatus.ADJUSTED_TO_FAIL + assert w.adjusted_cpi == Decimal(0) + assert w.adjusted_timestamp == adj_ts + + # Because the Product doesn't have the Wallet mode enabled, the + # user_payout fields should always be None + assert not user.product.user_wallet_config.enabled + assert s1.adjusted_user_payout is None + + def test_adjustment_session_values( + self, wall_manager, session_manager, session_factory, user + ): + # Completed Session with 2 wall events + s1 = session_factory( + user=user, + wall_count=2, + wall_req_cpi=Decimal(1), + wall_source=Source.DYNATA, + final_status=Status.COMPLETE, + started=started1, + ) + + session_manager.finish_with_status( + session=s1, + status=Status.COMPLETE, + status_code_1=StatusCode1.COMPLETE, + payout=Decimal("0.95"), + finished=finished2, + ) + + # Change the last wall event to a Failure + wall_manager.adjust_status( + wall=s1.wall_events[-1], + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL, + adjusted_timestamp=adj_ts, + ) + + # Refresh the Session with the new Wall Adjustment considerations, + session_manager.adjust_status(session=s1) + assert s1.status == Status.COMPLETE # Original status should remain + assert s1.adjusted_status == SessionAdjustedStatus.ADJUSTED_TO_FAIL + assert s1.adjusted_payout == Decimal(0) + assert s1.adjusted_timestamp == adj_ts + + # Because the Product doesn't have the Wallet mode enabled, the + # user_payout fields should always be None + assert not user.product.user_wallet_config.enabled + assert s1.adjusted_user_payout is None + + def test_double_adjustment_session_values( + self, wall_manager, session_manager, session_factory, user + ): + # Completed Session with 2 wall events + s1 = session_factory( + user=user, + wall_count=2, + wall_req_cpi=Decimal(1), + final_status=Status.COMPLETE, + started=started1, + ) + + session_manager.finish_with_status( + session=s1, + status=Status.COMPLETE, + status_code_1=StatusCode1.COMPLETE, + payout=Decimal("0.95"), + finished=finished2, + ) + + # Change the last wall event to a Failure + w: Wall = s1.wall_events[-1] + wall_manager.adjust_status( + wall=w, + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL, + adjusted_timestamp=adj_ts, + ) + + # Refresh the Session with the new Wall Adjustment considerations, + session_manager.adjust_status(session=s1) + + # Let's take that back again! Buyers love to do this. + # So now we're going to "un-reconcile" the last Wall Event which has + # already gone from a Complete >> Failure + wall_manager.adjust_status( + wall=w, adjusted_status=None, adjusted_timestamp=adj_ts2 + ) + assert w.adjusted_status is None + assert w.adjusted_cpi is None + assert w.adjusted_timestamp == adj_ts2 + + # Once the wall was unreconciled, "refresh" the Session again + assert s1.adjusted_status is not None + session_manager.adjust_status(session=s1) + assert s1.adjusted_status is None + assert s1.adjusted_payout is None + assert s1.adjusted_timestamp == adj_ts2 + assert s1.adjusted_user_payout is None + + def test_double_adjustment_sm_vs_db_values( + self, wall_manager, session_manager, session_factory, user + ): + # Completed Session with 2 wall events + s1 = session_factory( + user=user, + wall_count=2, + wall_req_cpi=Decimal(1), + wall_source=Source.DYNATA, + final_status=Status.COMPLETE, + started=started1, + ) + + session_manager.finish_with_status( + session=s1, + status=Status.COMPLETE, + status_code_1=StatusCode1.COMPLETE, + payout=Decimal("0.95"), + finished=finished2, + ) + + # Change the last wall event to a Failure + wall_manager.adjust_status( + wall=s1.wall_events[-1], + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL, + adjusted_timestamp=adj_ts, + ) + + # Refresh the Session with the new Wall Adjustment considerations, + session_manager.adjust_status(session=s1) + + # Let's take that back again! Buyers love to do this. + # So now we're going to "un-reconcile" the last Wall Event which has + # already gone from a Complete >> Failure + # Once the wall was unreconciled, "refresh" the Session again + wall_manager.adjust_status( + wall=s1.wall_events[-1], adjusted_status=None, adjusted_timestamp=adj_ts2 + ) + session_manager.adjust_status(session=s1) + + # Confirm that the sessions wall attributes are still aligned with + # what comes back directly from the database + db_wall_events = wall_manager.get_wall_events(session_id=s1.id) + for idx in range(len(s1.wall_events)): + w_sm: Wall = s1.wall_events[idx] + w_db: Wall = db_wall_events[idx] + + assert w_sm.uuid == w_db.uuid + assert w_sm.session_id == w_db.session_id + assert w_sm.status == w_db.status + assert w_sm.status_code_1 == w_db.status_code_1 + assert w_sm.status_code_2 == w_db.status_code_2 + + assert w_sm.elapsed == w_db.elapsed + + # Decimal("1.000000") vs Decimal(1) - based on mysql or postgres + assert pytest.approx(w_sm.cpi) == w_db.cpi + assert pytest.approx(w_sm.req_cpi) == w_db.req_cpi + + assert w_sm.model_dump_json( + exclude={"cpi", "req_cpi"} + ) == w_db.model_dump_json(exclude={"cpi", "req_cpi"}) + + def test_double_adjustment_double_completes( + self, wall_manager, session_manager, session_factory, user + ): + # Completed Session with 2 wall events + s1 = session_factory( + user=user, + wall_count=2, + wall_req_cpi=Decimal(2), + wall_source=Source.DYNATA, + final_status=Status.COMPLETE, + started=started1, + ) + + session_manager.finish_with_status( + session=s1, + status=Status.COMPLETE, + status_code_1=StatusCode1.COMPLETE, + payout=Decimal("0.95"), + finished=finished2, + ) + + # Change the last wall event to a Failure + wall_manager.adjust_status( + wall=s1.wall_events[-1], + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL, + adjusted_timestamp=adj_ts, + ) + + # Refresh the Session with the new Wall Adjustment considerations, + session_manager.adjust_status(session=s1) + + # Let's take that back again! Buyers love to do this. + # So now we're going to "un-reconcile" the last Wall Event which has + # already gone from a Complete >> Failure + # Once the wall was unreconciled, "refresh" the Session again + wall_manager.adjust_status( + wall=s1.wall_events[-1], adjusted_status=None, adjusted_timestamp=adj_ts2 + ) + session_manager.adjust_status(session=s1) + + # Reassign them - we already validated they're equal in previous + # tests so this is safe to do. + s1.wall_events = wall_manager.get_wall_events(session_id=s1.id) + + # The First Wall event was originally a Failure, now let's also set + # that as a complete, so now both Wall Events will b a + # complete (Fail >> Adj to Complete, Complete >> Adj to Fail >> Adj to Complete) + w1: Wall = s1.wall_events[0] + assert w1.status == Status.FAIL + assert w1.adjusted_status is None + assert w1.adjusted_cpi is None + assert w1.adjusted_timestamp is None + + wall_manager.adjust_status( + wall=w1, + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_COMPLETE, + adjusted_timestamp=adj_ts3, + ) + + assert w1.status == Status.FAIL # original status doesn't change + assert w1.adjusted_status == WallAdjustedStatus.ADJUSTED_TO_COMPLETE + assert w1.adjusted_cpi == w1.cpi + assert w1.adjusted_timestamp == adj_ts3 + + session_manager.adjust_status(s1) + assert SessionAdjustedStatus.PAYOUT_ADJUSTMENT == s1.adjusted_status + assert Decimal("3.80") == s1.adjusted_payout + assert s1.adjusted_user_payout is None + assert adj_ts3 == s1.adjusted_timestamp + + def test_complete_to_fail( + self, session_factory, user, session_manager, wall_manager, utc_hour_ago + ): + s1 = session_factory( + user=user, + wall_count=1, + wall_req_cpi=Decimal("1"), + final_status=Status.COMPLETE, + started=utc_hour_ago, + ) + + status, status_code_1 = s1.determine_session_status() + assert status == Status.COMPLETE + + thl_net = Decimal(sum(w.cpi for w in s1.wall_events if w.is_visible_complete())) + payout = user.product.determine_bp_payment(thl_net=thl_net) + + session_manager.finish_with_status( + session=s1, + status=status, + status_code_1=status_code_1, + finished=utc_hour_ago + timedelta(minutes=10), + payout=payout, + user_payout=None, + ) + + w1 = s1.wall_events[0] + w1.update( + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL, + adjusted_cpi=0, + adjusted_timestamp=adj_ts, + ) + assert w1.adjusted_status == WallAdjustedStatus.ADJUSTED_TO_FAIL + assert w1.adjusted_cpi == Decimal(0) + + new_status, new_payout, new_user_payout = s1.determine_new_status_and_payouts() + assert Status.FAIL == new_status + assert Decimal(0) == new_payout + + assert not user.product.user_wallet_config.enabled + assert new_user_payout is None + + s1.adjust_status() + assert SessionAdjustedStatus.ADJUSTED_TO_FAIL == s1.adjusted_status + assert Decimal(0) == s1.adjusted_payout + assert not user.product.user_wallet_config.enabled + assert s1.adjusted_user_payout is None + + # cpi adjustment + w1.update( + adjusted_status=WallAdjustedStatus.CPI_ADJUSTMENT, + adjusted_cpi=Decimal("0.69"), + adjusted_timestamp=adj_ts, + ) + assert WallAdjustedStatus.CPI_ADJUSTMENT == w1.adjusted_status + assert Decimal("0.69") == w1.adjusted_cpi + new_status, new_payout, new_user_payout = s1.determine_new_status_and_payouts() + assert Status.COMPLETE == new_status + assert Decimal("0.66") == new_payout + + assert not user.product.user_wallet_config.enabled + # assert Decimal("0.33") == new_user_payout + assert new_user_payout is None + + s1.adjust_status() + assert SessionAdjustedStatus.PAYOUT_ADJUSTMENT == s1.adjusted_status + assert Decimal("0.66") == s1.adjusted_payout + assert not user.product.user_wallet_config.enabled + # assert Decimal("0.33") == s1.adjusted_user_payout + assert s1.adjusted_user_payout is None + + # adjust cpi again + wall_manager.adjust_status( + wall=w1, + adjusted_status=WallAdjustedStatus.CPI_ADJUSTMENT, + adjusted_cpi=Decimal("0.50"), + adjusted_timestamp=adj_ts, + ) + assert WallAdjustedStatus.CPI_ADJUSTMENT == w1.adjusted_status + assert Decimal("0.50") == w1.adjusted_cpi + new_status, new_payout, new_user_payout = s1.determine_new_status_and_payouts() + assert Status.COMPLETE == new_status + assert Decimal("0.48") == new_payout + assert not user.product.user_wallet_config.enabled + # assert Decimal("0.24") == new_user_payout + assert new_user_payout is None + + s1.adjust_status() + assert SessionAdjustedStatus.PAYOUT_ADJUSTMENT == s1.adjusted_status + assert Decimal("0.48") == s1.adjusted_payout + assert not user.product.user_wallet_config.enabled + # assert Decimal("0.24") == s1.adjusted_user_payout + assert s1.adjusted_user_payout is None + + def test_complete_to_fail_to_complete(self, user, session_factory, utc_hour_ago): + # Setup: Complete, then adjust it to fail + s1 = session_factory( + user=user, + wall_count=1, + wall_req_cpi=Decimal("1"), + final_status=Status.COMPLETE, + started=utc_hour_ago, + ) + w1 = s1.wall_events[0] + + 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": utc_hour_ago + timedelta(minutes=10), + "payout": bp_pay, + "user_payout": user_pay, + } + ) + w1.update( + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL, + adjusted_cpi=0, + adjusted_timestamp=adj_ts, + ) + s1.adjust_status() + + # Test: Adjust back to complete + w1.update( + adjusted_status=None, + adjusted_cpi=None, + adjusted_timestamp=adj_ts, + ) + assert w1.adjusted_status is None + assert w1.adjusted_cpi is None + assert adj_ts == w1.adjusted_timestamp + + new_status, new_payout, new_user_payout = s1.determine_new_status_and_payouts() + assert Status.COMPLETE == new_status + assert Decimal("0.95") == new_payout + assert not user.product.user_wallet_config.enabled + # assert Decimal("0.48") == new_user_payout + assert new_user_payout is None + + s1.adjust_status() + assert s1.adjusted_status is None + assert s1.adjusted_payout is None + assert s1.adjusted_user_payout is None + + def test_complete_to_fail_to_complete_adj( + self, user, session_factory, utc_hour_ago + ): + s1 = session_factory( + user=user, + wall_count=2, + wall_req_cpis=[Decimal(1), Decimal(2)], + final_status=Status.COMPLETE, + started=utc_hour_ago, + ) + + w1 = s1.wall_events[0] + w2 = s1.wall_events[1] + + status, status_code_1 = s1.determine_session_status() + thl_net = Decimal(sum(w.cpi for w in s1.wall_events if w.is_visible_complete())) + payout = user.product.determine_bp_payment(thl_net=thl_net) + s1.update( + **{ + "status": status, + "status_code_1": status_code_1, + "finished": utc_hour_ago + timedelta(minutes=25), + "payout": payout, + "user_payout": None, + } + ) + + # Test. Adjust first fail to complete. Now we have 2 completes. + w1.update( + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_COMPLETE, + adjusted_cpi=w1.cpi, + adjusted_timestamp=adj_ts, + ) + s1.adjust_status() + assert SessionAdjustedStatus.PAYOUT_ADJUSTMENT == s1.adjusted_status + assert Decimal("2.85") == s1.adjusted_payout + assert not user.product.user_wallet_config.enabled + # assert Decimal("1.42") == s1.adjusted_user_payout + assert s1.adjusted_user_payout is None + + # Now we have [Fail, Complete ($2)] -> [Complete ($1), Fail] + w2.update( + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL, + adjusted_cpi=0, + adjusted_timestamp=adj_ts2, + ) + s1.adjust_status() + assert SessionAdjustedStatus.PAYOUT_ADJUSTMENT == s1.adjusted_status + assert Decimal("0.95") == s1.adjusted_payout + assert not user.product.user_wallet_config.enabled + # assert Decimal("0.48") == s1.adjusted_user_payout + assert s1.adjusted_user_payout is None + + def test_complete_to_fail_to_complete_adj1( + self, user, session_factory, utc_hour_ago + ): + # Same as test_complete_to_fail_to_complete_adj but in opposite order + s1 = session_factory( + user=user, + wall_count=2, + wall_req_cpis=[Decimal(1), Decimal(2)], + final_status=Status.COMPLETE, + started=utc_hour_ago, + ) + + w1 = s1.wall_events[0] + w2 = s1.wall_events[1] + + status, status_code_1 = s1.determine_session_status() + thl_net = Decimal(sum(w.cpi for w in s1.wall_events if w.is_visible_complete())) + payout = user.product.determine_bp_payment(thl_net) + s1.update( + **{ + "status": status, + "status_code_1": status_code_1, + "finished": utc_hour_ago + timedelta(minutes=25), + "payout": payout, + "user_payout": None, + } + ) + + # Test. Adjust complete to fail. Now we have 2 fails. + w2.update( + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL, + adjusted_cpi=0, + adjusted_timestamp=adj_ts, + ) + s1.adjust_status() + assert SessionAdjustedStatus.ADJUSTED_TO_FAIL == s1.adjusted_status + assert Decimal(0) == s1.adjusted_payout + assert not user.product.user_wallet_config.enabled + # assert Decimal(0) == s.adjusted_user_payout + assert s1.adjusted_user_payout is None + # Now we have [Fail, Complete ($2)] -> [Complete ($1), Fail] + w1.update( + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_COMPLETE, + adjusted_cpi=w1.cpi, + adjusted_timestamp=adj_ts2, + ) + s1.adjust_status() + assert SessionAdjustedStatus.PAYOUT_ADJUSTMENT == s1.adjusted_status + assert Decimal("0.95") == s1.adjusted_payout + assert not user.product.user_wallet_config.enabled + # assert Decimal("0.48") == s.adjusted_user_payout + assert s1.adjusted_user_payout is None + + def test_fail_to_complete_to_fail(self, user, session_factory, utc_hour_ago): + # End with an abandon + s1 = session_factory( + user=user, + wall_count=2, + wall_req_cpis=[Decimal(1), Decimal(2)], + final_status=Status.ABANDON, + started=utc_hour_ago, + ) + + w1 = s1.wall_events[0] + w2 = s1.wall_events[1] + + # abandon adjust to complete + w2.update( + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_COMPLETE, + adjusted_cpi=w2.cpi, + adjusted_timestamp=adj_ts, + ) + assert WallAdjustedStatus.ADJUSTED_TO_COMPLETE == w2.adjusted_status + s1.adjust_status() + assert SessionAdjustedStatus.ADJUSTED_TO_COMPLETE == s1.adjusted_status + assert Decimal("1.90") == s1.adjusted_payout + assert not user.product.user_wallet_config.enabled + # assert Decimal("0.95") == s1.adjusted_user_payout + assert s1.adjusted_user_payout is None + + # back to fail + w2.update( + adjusted_status=None, + adjusted_cpi=None, + adjusted_timestamp=adj_ts, + ) + assert w2.adjusted_status is None + s1.adjust_status() + assert s1.adjusted_status is None + assert s1.adjusted_payout is None + assert s1.adjusted_user_payout is None + + # other is now complete + w1.update( + adjusted_status=WallAdjustedStatus.ADJUSTED_TO_COMPLETE, + adjusted_cpi=w1.cpi, + adjusted_timestamp=adj_ts, + ) + assert WallAdjustedStatus.ADJUSTED_TO_COMPLETE == w1.adjusted_status + s1.adjust_status() + assert SessionAdjustedStatus.ADJUSTED_TO_COMPLETE == s1.adjusted_status + assert Decimal("0.95") == s1.adjusted_payout + assert not user.product.user_wallet_config.enabled + # assert Decimal("0.48") == s1.adjusted_user_payout + assert s1.adjusted_user_payout is None |
