diff options
| author | stuppie | 2026-09-04 15:15:24 -0600 |
|---|---|---|
| committer | stuppie | 2026-09-04 15:15:24 -0600 |
| commit | 48687643a8e11ce8f7f01cf5e721d4bb7202cd82 (patch) | |
| tree | c2607bfb268ea0996e32bb6a1335defed18e1a24 | |
| parent | 9ed9d9822d006a0d71bf7559c2ad0968bb243c8c (diff) | |
| download | generalresearch-48687643a8e11ce8f7f01cf5e721d4bb7202cd82.tar.gz generalresearch-48687643a8e11ce8f7f01cf5e721d4bb7202cd82.zip | |
handle LedgerTransactionReleaseLockError in business_payout_event_manager.create_bp_payout_event. Fix related tests.
| -rw-r--r-- | generalresearch/managers/thl/ledger_manager/thl_ledger.py | 5 | ||||
| -rw-r--r-- | generalresearch/managers/thl/payout.py | 56 | ||||
| -rw-r--r-- | tests/managers/thl/test_ledger/test_thl_lm_bp_payout.py | 126 |
3 files changed, 86 insertions, 101 deletions
diff --git a/generalresearch/managers/thl/ledger_manager/thl_ledger.py b/generalresearch/managers/thl/ledger_manager/thl_ledger.py index 0f8c6d3..e5a308f 100644 --- a/generalresearch/managers/thl/ledger_manager/thl_ledger.py +++ b/generalresearch/managers/thl/ledger_manager/thl_ledger.py @@ -830,12 +830,13 @@ class ThlLedgerManager(LedgerManager): created: datetime, ) -> LedgerTransaction: + tx_type = TransactionType.BP_PAYOUT metadata = { - tmc.TX_TYPE: TransactionType.BP_PAYOUT, + tmc.TX_TYPE: tx_type, tmc.EVENT: payoutevent_uuid, } # This tag will uniquely identify this tx - tag = f"{self.currency.value}:bp_payout:{payoutevent_uuid}" + tag = f"{self.currency.value}:{tx_type.value}:{payoutevent_uuid}" cash_account = self.get_account_cash() bp_wallet_account = self.get_account_or_create_bp_wallet(product) diff --git a/generalresearch/managers/thl/payout.py b/generalresearch/managers/thl/payout.py index 5397ff4..59dc2e9 100644 --- a/generalresearch/managers/thl/payout.py +++ b/generalresearch/managers/thl/payout.py @@ -18,12 +18,14 @@ from generalresearch.managers.base import ( ) from generalresearch.managers.thl.ledger_manager.exceptions import ( LedgerTransactionConditionFailedError, + LedgerTransactionReleaseLockError, ) from generalresearch.models.custom_types import AwareDatetimeISO, UUIDStr from generalresearch.models.thl.definitions import PayoutStatus from generalresearch.models.thl.ledger import ( Direction, OrderBy, + TransactionType, ) from generalresearch.models.thl.payout import ( BrokerageProductPayoutEvent, @@ -97,9 +99,9 @@ class PayoutEventManager(PostgresManagerWithRedis): with self.pg_config.make_connection() as conn: with conn.cursor() as c: c.execute(query=query, params=d) - assert ( - c.rowcount == 1 - ), "Nothing was updated! Are you sure this payout_event exists?" + assert c.rowcount == 1, ( + "Nothing was updated! Are you sure this payout_event exists?" + ) conn.commit() @@ -567,6 +569,13 @@ class BrokerageProductPayoutEventManager(PayoutEventManager): """) from e self.update(payout_event=bp_pe, status=PayoutStatus.FAILED) raise + except LedgerTransactionReleaseLockError as e: + # Redis error upon lock release. The tx was most likely created. + LOG.warning(e) + tag = f"{thl_ledger_manager.currency.value}:{TransactionType.BP_PAYOUT.value}:{bp_pe.uuid}" + # Check if it was created, and if so, swallow error. + if not thl_ledger_manager.get_tx_ids_by_tag(tag=tag): + self.update(payout_event=bp_pe, status=PayoutStatus.FAILED) except Exception: self.update(payout_event=bp_pe, status=PayoutStatus.FAILED) raise @@ -632,9 +641,9 @@ class BusinessPayoutEventManager(PostgresManagerWithRedis): for bp_payout in d["bp_payouts"]: bp_payout["created"] = datetime.fromisoformat(bp_payout["created"]) bpe = BusinessPayoutEvent.model_validate(d) - assert ( - bpe.bp_payouts is not None and len(bpe.bp_payouts) > 0 - ), "No BP payouts found for this Business Payout Event. This shouldn't happen!" + assert bpe.bp_payouts is not None and len(bpe.bp_payouts) > 0, ( + "No BP payouts found for this Business Payout Event. This shouldn't happen!" + ) return bpe def filter_by( @@ -679,9 +688,9 @@ class BusinessPayoutEventManager(PostgresManagerWithRedis): for bp_payout in row["bp_payouts"]: bp_payout["created"] = datetime.fromisoformat(bp_payout["created"]) bpe = BusinessPayoutEvent.model_validate(row) - assert ( - bpe.bp_payouts is not None and len(bpe.bp_payouts) > 0 - ), "No BP payouts found for this Business Payout Event. This shouldn't happen!" + assert bpe.bp_payouts is not None and len(bpe.bp_payouts) > 0, ( + "No BP payouts found for this Business Payout Event. This shouldn't happen!" + ) bpes.append(bpe) return bpes @@ -698,9 +707,9 @@ class BusinessPayoutEventManager(PostgresManagerWithRedis): for bp_pe in bpe.bp_payouts ] txs = thl_lm.get_tx_ids_by_tags(tags=tags) - assert len(txs) == len( - bpe.bp_payouts - ), f"Expected {len(bpe.bp_payouts)} BP payouts but found {len(txs)}!" + assert len(txs) == len(bpe.bp_payouts), ( + f"Expected {len(bpe.bp_payouts)} BP payouts but found {len(txs)}!" + ) return True def resume_failed_business_payout( @@ -826,9 +835,9 @@ class BusinessPayoutEventManager(PostgresManagerWithRedis): shortfall: int = int(target_amount) - w_df["deduction"].sum() w_df["remaining_balance"] = w_df["available_balance"] - w_df["deduction"] - assert w_df[ - w_df["deduction"] > w_df["available_balance"] - ].empty, "Trying to deduct more from an Product than what is available" + assert w_df[w_df["deduction"] > w_df["available_balance"]].empty, ( + "Trying to deduct more from an Product than what is available" + ) return w_df @@ -900,9 +909,9 @@ class BusinessPayoutEventManager(PostgresManagerWithRedis): ) -> BusinessPayoutEvent: assert isinstance(bpe, BusinessPayoutEventCreate) assert bpe.bp_payouts, "Must provide at least one BP Payout" - assert {bp_pe.status for bp_pe in bpe.bp_payouts} == { - PayoutStatus.PENDING - }, "All BP Payouts must be PENDING" + assert {bp_pe.status for bp_pe in bpe.bp_payouts} == {PayoutStatus.PENDING}, ( + "All BP Payouts must be PENDING" + ) INSERT_SUPPLIER_PAYOUT = """ INSERT INTO supplier_payout ( business_id, created, amount, @@ -995,9 +1004,9 @@ class BusinessPayoutEventManager(PostgresManagerWithRedis): # Can't pay any Products that don't have a remaining balance df = df[df["remaining_balance"] > 0].copy() - assert ( - df.deduction.sum() == business.balance.recoup - ), "recoup_proportional failure" + assert df.deduction.sum() == business.balance.recoup, ( + "recoup_proportional failure" + ) df["issue_amount"] = BusinessPayoutEventManager.distribute_amount( df=df, amount=amount @@ -1103,8 +1112,7 @@ class BusinessPayoutEventManager(PostgresManagerWithRedis): account = thl_ledger_manager.get_account( f"{thl_ledger_manager.currency.value}:bp_wallet:{product.uuid}" ) - bpe = BusinessPayoutEvent( - id=None, + bpe = BusinessPayoutEventCreate( business_id=product.business_uuid, payout_type=PayoutType.ACH, amount=amount, @@ -1125,7 +1133,7 @@ class BusinessPayoutEventManager(PostgresManagerWithRedis): ) ], ) - self.create_business_payout_event(bpe=bpe) + bpe = self.create_business_payout_event(bpe=bpe) self.bp_pe_manager.create_tx_bp_payout_from_payout_event( thl_ledger_manager=thl_ledger_manager, bp_pe=bpe.bp_payouts[0], 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 index de368d8..27ddc29 100644 --- a/tests/managers/thl/test_ledger/test_thl_lm_bp_payout.py +++ b/tests/managers/thl/test_ledger/test_thl_lm_bp_payout.py @@ -32,13 +32,13 @@ from generalresearch.models.thl.session import ( StatusCode1, Wall, ) -from generalresearch.models.thl.wallet.definitions import PayoutType from generalresearch.redis_helper import RedisConfig if TYPE_CHECKING: from generalresearch.currency import LedgerCurrency from generalresearch.managers.thl.payout import ( BrokerageProductPayoutEventManager, + BusinessPayoutEventManager, ) from generalresearch.models.thl.product import Product from generalresearch.models.thl.user import User @@ -353,14 +353,13 @@ class TestThlLedgerManagerBPPayout: # Create TX will fail on lock exit, after the tx was created! with monkeypatch.context() as m: m.setattr(Lock, "release", broken_release) - with pytest.raises(expected_exception=Exception) as e: + with pytest.raises(LedgerTransactionReleaseLockError) as e: thl_ledger_manager.create_tx_bp_payout( product=product, amount=rand_amount, payoutevent_uuid=payoutevent_uuid, created=datetime.now(tz=UTC), ) - assert e.type is LedgerTransactionReleaseLockError assert str(e.value) == "Redis error: Simulated timeout during release" # Transaction was still created! @@ -372,11 +371,16 @@ class TestThlLedgerManagerBPPayout: class TestPayoutEventManagerBPPayout: + @pytest.fixture(autouse=True) + def setup(self, create_main_accounts): + create_main_accounts() + def test_create( self, product: Product, thl_ledger_manager: ThlLedgerManager, brokerage_product_payout_event_manager: BrokerageProductPayoutEventManager, + business_payout_event_manager: BusinessPayoutEventManager, ): rand_amount: USDCent = USDCent(randint(100, 1_000)) now = datetime.now(tz=UTC) @@ -389,18 +393,17 @@ class TestPayoutEventManagerBPPayout: ) assert thl_ledger_manager.get_account_balance(bp_wallet_account) == rand_amount - pe = brokerage_product_payout_event_manager.create_bp_payout_event( + bpe = business_payout_event_manager.create_bp_payout_event( thl_ledger_manager=thl_ledger_manager, product=product, created=now, amount=rand_amount, - payout_type=PayoutType.ACH, + ext_ref_id=uuid4().hex, ) + bp_pe = bpe.bp_payouts[0] assert brokerage_product_payout_event_manager.check_for_ledger_tx( thl_ledger_manager=thl_ledger_manager, - product_id=product.id, - amount=rand_amount, - payout_event=pe, + payout_event=bp_pe, ) assert thl_ledger_manager.get_account_balance(bp_wallet_account) == 0 @@ -410,10 +413,11 @@ class TestPayoutEventManagerBPPayout: caplog, thl_ledger_manager: ThlLedgerManager, brokerage_product_payout_event_manager: BrokerageProductPayoutEventManager, + business_payout_event_manager: BusinessPayoutEventManager, + monkeypatch: pytest.MonkeyPatch, ): caplog.set_level("WARNING") - original_acquire = Lock.acquire - original_release = Lock.release + ext_ref_id = uuid4().hex rand_amount: USDCent = USDCent(randint(100, 1_000)) now = datetime.now(tz=UTC) @@ -427,21 +431,17 @@ class TestPayoutEventManagerBPPayout: assert thl_ledger_manager.get_account_balance(bp_wallet_account) == rand_amount # 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_ledger_manager, - product=product, - created=now, - amount=rand_amount, - payout_type=PayoutType.ACH, - ) - assert e.type is LedgerTransactionCreateError + with monkeypatch.context() as m: + m.setattr(Lock, "acquire", broken_acquire) + with pytest.raises(LedgerTransactionCreateError) as e: + business_payout_event_manager.create_bp_payout_event( + thl_ledger_manager=thl_ledger_manager, + product=product, + created=now, + amount=rand_amount, + ext_ref_id=ext_ref_id, + ) 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_ledger_manager.get_tx_filtered_by_account( account_uuid=bp_wallet_account.uuid @@ -451,21 +451,18 @@ class TestPayoutEventManagerBPPayout: assert len(txs) == 0 pes = ( brokerage_product_payout_event_manager.get_bp_bp_payout_events_for_products( - thl_ledger_manager=thl_ledger_manager, product_uuids=[product.id] + 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_ledger_manager, - payout_event_uuid=pe.uuid, + bp_pe=pe, ) txs = thl_ledger_manager.get_tx_filtered_by_account( account_uuid=bp_wallet_account.uuid @@ -475,44 +472,28 @@ class TestPayoutEventManagerBPPayout: assert thl_ledger_manager.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( + with pytest.raises(expected_exception=ValueError) as e: + pe = business_payout_event_manager.create_bp_payout_event( thl_ledger_manager=thl_ledger_manager, product=product, created=now, amount=rand_amount, - payout_type=PayoutType.ACH, + ext_ref_id=ext_ref_id, ) - assert e.type is ValueError - assert "Payout event already exists!" in str(e.value) + assert ( + "Cannot create a BusinessPayoutEvent with an existing transaction_id" + 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=UTC) - with pytest.raises(LedgerTransactionConditionFailedError) as e: - pe = brokerage_product_payout_event_manager.create_bp_payout_event( - thl_ledger_manager=thl_ledger_manager, - 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=UTC) - pe = brokerage_product_payout_event_manager.create_bp_payout_event( + # Change the ext_ref_id so it'll create a new payout event + pe = business_payout_event_manager.create_bp_payout_event( thl_ledger_manager=thl_ledger_manager, product=product, created=now, amount=rand_amount, - payout_type=PayoutType.ACH, - skip_one_per_day_check=True, - skip_wallet_balance_check=True, + ext_ref_id=uuid4().hex, ) txs = thl_ledger_manager.get_tx_filtered_by_account( @@ -525,20 +506,17 @@ class TestPayoutEventManagerBPPayout: thl_ledger_manager.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: Product, - caplog, thl_ledger_manager: ThlLedgerManager, + business_payout_event_manager: BusinessPayoutEventManager, brokerage_product_payout_event_manager: BrokerageProductPayoutEventManager, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ): caplog.set_level("WARNING") - original_release = Lock.release - rand_amount: USDCent = USDCent(randint(100, 1_000)) now = datetime.now(tz=UTC) bp_wallet_account = thl_ledger_manager.get_account_or_create_bp_wallet( @@ -553,18 +531,17 @@ class TestPayoutEventManagerBPPayout: # 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 - brokerage_product_payout_event_manager.create_bp_payout_event( - thl_ledger_manager=thl_ledger_manager, - 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 - ) + caplog.clear() + with monkeypatch.context() as m, caplog.at_level("WARNING"): + m.setattr(Lock, "release", broken_release) + business_payout_event_manager.create_bp_payout_event( + thl_ledger_manager=thl_ledger_manager, + product=product, + created=now, + amount=rand_amount, + ext_ref_id=uuid4().hex, + ) + assert "Redis error: Simulated timeout during release" in caplog.messages txs = thl_ledger_manager.get_tx_filtered_by_account( account_uuid=bp_wallet_account.uuid @@ -573,9 +550,8 @@ class TestPayoutEventManagerBPPayout: assert len(txs) == 1 pes = ( brokerage_product_payout_event_manager.get_bp_bp_payout_events_for_products( - thl_ledger_manager=thl_ledger_manager, product_uuids=[product.uuid] + product_uuids=[product.uuid] ) ) assert len(pes) == 1 assert pes[0].status == PayoutStatus.COMPLETE - Lock.release = original_release |
