aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--generalresearch/managers/thl/payout.py379
-rw-r--r--generalresearch/models/gr/business.py1
-rw-r--r--generalresearch/models/thl/payout.py64
-rw-r--r--generalresearch/models/thl/product.py1
-rw-r--r--tests/managers/thl/test_payout.py131
5 files changed, 254 insertions, 322 deletions
diff --git a/generalresearch/managers/thl/payout.py b/generalresearch/managers/thl/payout.py
index 4b08be8..8f5b256 100644
--- a/generalresearch/managers/thl/payout.py
+++ b/generalresearch/managers/thl/payout.py
@@ -398,37 +398,23 @@ class BrokerageProductPayoutEventManager(PayoutEventManager):
def get_by_uuid(
self,
pe_uuid: UUIDStr,
- # --- Support resources ---
- account_product_mapping: dict[UUIDStr, UUIDStr] | None = None,
) -> BrokerageProductPayoutEvent:
res = self.pg_config.execute_sql_query(
query="""
- SELECT ep.uuid,
- ep.debit_account_uuid,
- ep.cashout_method_uuid,
+ SELECT ep.uuid, ep.debit_account_uuid, ep.cashout_method_uuid,
ep.created, ep.amount, ep.status, ep.ext_ref_id, ep.payout_type,
ep.request_data::jsonb,
- ep.order_data::jsonb
+ ep.order_data::jsonb,
+ la.reference_uuid as product_id
FROM event_payout AS ep
+ JOIN ledger_account la on la.uuid = debit_account_uuid
WHERE ep.uuid = %s
""",
params=[pe_uuid],
)
assert len(res) == 1, f"{pe_uuid} expected 1 result, got {len(res)}"
-
- d = res[0]
-
- # This isn't really need for creation... but we're doing it so that
- # it can return back a full BrokerageProductPayoutEvent instance
- if account_product_mapping is None:
- rc = self.redis_client
- account_product_mapping: dict = rc.hgetall(name="pem:account_to_product")
- assert isinstance(account_product_mapping, dict)
-
- d["product_id"] = account_product_mapping[d["debit_account_uuid"]]
-
- return BrokerageProductPayoutEvent.model_validate(d)
+ return BrokerageProductPayoutEvent.model_validate(res[0])
@staticmethod
def check_for_ledger_tx(
@@ -478,112 +464,78 @@ class BrokerageProductPayoutEventManager(PayoutEventManager):
def filter_by(
self,
- reference_uuid: str | None = None,
ext_ref_id: str | None = None,
debit_account_uuids: Collection[UUIDStr] | None = None,
amount: int | None = None,
created: datetime | None = None,
created_after: datetime | None = None,
- product_ids: str | None = None,
- bp_user_ids: Collection[str] | None = None,
+ product_ids: Collection[str] | None = None,
cashout_types: Collection[PayoutType] | None = None,
statuses: Collection[PayoutStatus] | None = None,
) -> list[BrokerageProductPayoutEvent]:
- """Try to retrieve payout events by the product_id/user_uuid, amount,
- and optionally timestamp.
+ """Try to retrieve BP payout events.
WARNING: This is only on the "payout events" table and nothing to
- do with the Ledger itself. Therefore, the product_ids query
- doesn't return Brokerage Product Payouts (the ACH or Wire events
- to Suppliers) as part of the query.
+ do with the Ledger itself
- *** IT IS ONLY FOR USER PAYOUTS ***
-
- Note: what used to be in thl-grpcs "ListCashoutRequests" calling
- "list_cashout_requests" was merged into this.
+ *** IT IS ONLY FOR Brokerage Product PAYOUTS ***
"""
- args = []
+ params = dict()
filters = []
- if reference_uuid:
- # This could be a product_id or a user_uuid
- filters.append("la.reference_uuid = %s")
- args.append(reference_uuid)
if ext_ref_id:
- # This is transaction id for tracking ACH/Wires with a banking
- # institution
- filters.append("ep.ext_ref_id = %s")
- args.append(ext_ref_id)
+ # This is transaction id for tracking ACH/Wires with a banking institution
+ filters.append("ep.ext_ref_id = %(ext_ref_id)s")
+ params["ext_ref_id"] = ext_ref_id
if debit_account_uuids:
- # Or we could use the bp_wallet or user_wallet's account uuid
- # instead of looking up by the product/user
- filters.append("ep.debit_account_uuid = ANY(%s)")
- args.append(debit_account_uuids)
+ # Or we could use the bp_wallet's account uuid
+ # instead of looking up by the product
+ filters.append("ep.debit_account_uuid = ANY(%(debit_account_uuids)s)")
+ params["debit_account_uuids"] = debit_account_uuids
if amount:
- filters.append("ep.amount = %s")
- args.append(amount)
+ filters.append("ep.amount = %(amount)s")
+ params["amount"] = amount
if created:
- filters.append("ep.created = %s")
- args.append(created.replace(tzinfo=None))
+ filters.append("ep.created = %(created)s")
+ params["created"] = created
if created_after:
- filters.append("ep.created >= %s")
- args.append(created_after.replace(tzinfo=None))
- if product_ids:
- filters.append("product_id = ANY(%s)")
- args.append(product_ids)
- if bp_user_ids:
- filters.append("product_user_id = ANY(%s)")
- args.append(bp_user_ids)
- if cashout_types:
- filters.append("payout_type = ANY(%s)")
- args.append([x.value for x in cashout_types])
- if statuses:
- filters.append("status = ANY(%s)")
- args.append([x.value for x in statuses])
+ filters.append("ep.created >= %(created_after)s")
+ params["created_after"] = created_after
+ if product_ids is not None:
+ filters.append("la.reference_uuid = ANY(%(product_ids)s)")
+ params["product_ids"] = product_ids
+ if cashout_types is not None:
+ filters.append("payout_type = ANY(%(cashout_types)s)")
+ params["cashout_types"] = [x.value for x in cashout_types]
+ if statuses is not None:
+ filters.append("status = ANY(%(statuses)s)")
+ params["statuses"] = [x.value for x in statuses]
assert len(filters) > 0, "must pass at least 1 filter"
filter_str = " AND ".join(filters)
+ params["cashout_method_uuid"] = self.CASHOUT_METHOD_UUID
res = self.pg_config.execute_sql_query(
query=f"""
- SELECT ep.uuid,
- ep.debit_account_uuid,
- ep.cashout_method_uuid,
- ep.created,
- ep.amount, ep.status, ep.ext_ref_id, ep.payout_type,
+ SELECT ep.uuid, ep.debit_account_uuid, ep.cashout_method_uuid,
+ ep.created, ep.amount, ep.status, ep.ext_ref_id,
+ ep.payout_type, ep.supplier_payout_id,
ep.request_data::jsonb, ep.order_data::jsonb,
ac.name as description,
- la.reference_type as account_reference_type,
- la.reference_uuid as account_reference_uuid
+ la.reference_uuid as product_id
FROM event_payout AS ep
LEFT JOIN accounting_cashoutmethod AS ac
ON ep.cashout_method_uuid = ac.id
LEFT JOIN ledger_account AS la
ON la.uuid = ep.debit_account_uuid
- LEFT JOIN thl_user u
- ON la.reference_uuid = u.uuid
- WHERE cashout_method_uuid = '{self.CASHOUT_METHOD_UUID}'
+ WHERE cashout_method_uuid = %(cashout_method_uuid)s
+ AND la.reference_type = 'bp'
AND {filter_str}
""",
- params=args,
+ params=params,
)
-
- rc = self.redis_client
- account_product_mapping = rc.hgetall(name="pem:account_to_product")
-
pes = []
- for d in res:
- for k in [
- "uuid",
- "debit_account_uuid",
- "account_reference_uuid",
- "cashout_method_uuid",
- ]:
- if d[k] is not None:
- d[k] = UUID(d[k]).hex
-
- d["product_id"] = account_product_mapping[d["debit_account_uuid"]]
- pes.append(BrokerageProductPayoutEvent.model_validate(d))
-
+ for row in res:
+ pes.append(BrokerageProductPayoutEvent.model_validate(row))
return pes
def get_bp_payout_events_for_accounts(
@@ -596,53 +548,38 @@ class BrokerageProductPayoutEventManager(PayoutEventManager):
def get_bp_bp_payout_events_for_products(
self,
- thl_ledger_manager: ThlLedgerManager,
product_uuids: Collection[UUIDStr],
order_by: OrderBy | None = OrderBy.ASC,
) -> list[BrokerageProductPayoutEvent]:
"""This is a terrible name, but it returns the
BPPayoutEvent model type rather than a list of PayoutEvents.
- We do this for the Supplier centric APIs where they don't know,
+ We do this for the Supplier-centric APIs where they don't know
or care about the underlying ledger account structure.
"""
assert len(product_uuids) > 0, "Must provide product_uuids"
- accounts = thl_ledger_manager.get_accounts_bp_wallet_for_products(
- product_uuids=product_uuids
- )
-
- assert len(accounts) == len(product_uuids), "Unequal Product & Account lists"
-
- rc = self.redis_client
- account_product_mapping = rc.hgetall(name="pem:account_to_product")
+ order_by = order_by or OrderBy.ASC
- payout_events: list[BrokerageProductPayoutEvent] = (
- self.get_bp_payout_events_for_accounts(
- accounts=accounts,
- )
+ payout_events = self.filter_by(
+ product_ids=product_uuids,
+ cashout_types=[PayoutType.ACH],
)
-
- return BrokerageProductPayoutEvent.from_payout_events(
- payout_events=payout_events,
- account_product_mapping=account_product_mapping,
- order_by=order_by,
+ payout_events = sorted(
+ payout_events, key=lambda x: x.created, reverse=order_by == OrderBy.DESC
)
+ return payout_events
def retry_create_bp_payout_event_tx(
self,
thl_ledger_manager: ThlLedgerManager,
product: Product,
- payout_event_uuid: UUIDStr,
- skip_wallet_balance_check: bool = False,
- skip_one_per_day_check: bool = False,
+ bp_pe: BrokerageProductPayoutEvent,
) -> BrokerageProductPayoutEvent:
"""
If a create_bp_payout_event call fails, this can be called with
the associated payoutevent.
"""
- bp_pe: BrokerageProductPayoutEvent = self.get_by_uuid(payout_event_uuid)
assert bp_pe.status == PayoutStatus.FAILED, "Only use this on failed payouts"
- created = bp_pe.created
assert not self.check_for_ledger_tx(
thl_ledger_manager=thl_ledger_manager,
@@ -655,10 +592,8 @@ class BrokerageProductPayoutEventManager(PayoutEventManager):
thl_ledger_manager=thl_ledger_manager,
bp_pe=bp_pe,
product=product,
- amount=bp_pe.amount_usd,
- created=created,
- skip_one_per_day_check=skip_one_per_day_check,
- skip_wallet_balance_check=skip_wallet_balance_check,
+ skip_one_per_day_check=True,
+ skip_wallet_balance_check=True,
)
def create_pending_bp_payout_events(
@@ -763,134 +698,102 @@ class BrokerageProductPayoutEventManager(PayoutEventManager):
class BusinessPayoutEventManager(BrokerageProductPayoutEventManager):
- def update_ext_reference_ids(
- self,
- new_value: str,
- current_value: str | None = None,
- ) -> None:
- """
- There are scenarios where an ACH/Wire payout event was saved with
- a generic or anonymized reference identifier. We may want to be
- able to go back and update all of those transaction IDs.
+ def get_by_ext_ref_id(self, ext_ref_id: str) -> BusinessPayoutEvent:
+ res = self.pg_config.execute_sql_query(
+ """
+ SELECT
+ sp.*,
+ ep.bp_payouts
+ FROM supplier_payout sp
+ JOIN (
+ SELECT
+ ep_inner.supplier_payout_id,
+ jsonb_agg(
+ to_jsonb(ep_inner)
+ || jsonb_build_object('product_id', la.reference_uuid)
+ ORDER BY ep_inner.created
+ ) AS bp_payouts
+ FROM event_payout ep_inner
+ JOIN ledger_account la
+ ON ep_inner.debit_account_uuid = la.uuid
+ GROUP BY ep_inner.supplier_payout_id
+ ) ep ON sp.id = ep.supplier_payout_id
+ WHERE sp.ext_ref_id = %(ext_ref_id)s
+ """,
+ {"ext_ref_id": ext_ref_id},
+ )
+ assert len(res) == 1, f"No Business Payout found with ext ref: {ext_ref_id}"
+ d = res[0]
+ 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!"
+ return bpe
+ def validate_business_payout_in_ledger(
+ self, ext_ref_id: str, thl_lm: ThlLedgerManager
+ ):
"""
-
- if current_value is None:
- raise ValueError("Dangerous to do ambiguous updates")
-
- # SELECT first to check that records exist
- res = self.filter_by(ext_ref_id=current_value)
- if len(res) == 0:
- raise Warning("No event_payouts found to UPDATE")
-
- # As of 2025, no single Business has more than 10,000 Products,
- # leave the limit in as an additional safeguard.
- query = """
- UPDATE event_payout
- SET ext_ref_id = %s
- WHERE ext_ref_id = %s
+ Check that there exist ledger TXs for the BP payouts
"""
- with self.pg_config.make_connection() as conn:
- with conn.cursor() as c:
- c.execute(query=query, params=[new_value, current_value])
- assert c.rowcount < 10000
- conn.commit()
+ bpe = self.get_by_ext_ref_id(ext_ref_id=ext_ref_id)
+ account_uuids = [bp_pe.debit_account_uuid for bp_pe in bpe.bp_payouts]
+ txs = thl_lm.get_tx_bp_payouts(account_uuids=account_uuids)
+ assert len(txs) == len(
+ bpe.bp_payouts
+ ), f"Expected {len(bpe.bp_payouts)} BP payouts but found {len(txs)}!"
+ return True
- def delete_failed_business_payout(self, ext_ref_id: str, thl_lm: ThlLedgerManager):
+ def resume_failed_business_payout(
+ self, ext_ref_id: str, thl_lm: ThlLedgerManager, pm: ProductManager
+ ):
"""
- Sometimes ACH/Wire payouts fail due to multiple reasons (timeouts,
- Business Product having insufficient funds, etc). This is a utility
- method that finds all event_payouts, and deletes them with all the
- associated:
- (1) Transactions
- (2) Transaction Metadata
- (3) Transaction Entries
-
- and then proceeds to delete them all in reverse order (so there is
- no orphan / FK constraint issues).
+ Sometimes a business payout's BP payouts fail due to multiple reasons
+ (timeouts, BP having insufficient funds, etc). Grab the PENDING
+ BP payout events and retry them.
"""
+ bpe = self.get_by_ext_ref_id(ext_ref_id=ext_ref_id)
+ assert bpe.id
+ assert bpe.bp_payouts
- # (1) Find all by payout_event
- event_payouts = self.filter_by(ext_ref_id=ext_ref_id)
- if len(event_payouts) == 0:
- raise Warning("No event_payouts found to DELETE")
-
- # sum([i["amount"] for i in event_payouts])/100
- event_payout_uuids = [i.uuid for i in event_payouts]
-
- # (2) Find all ledger_transactions
- tags = [f"{thl_lm.currency.value}:bp_payout:{x}" for x in event_payout_uuids]
- transactions = thl_lm.get_txs_by_tags(tags=tags)
- transaction_ids = [tx.id for tx in transactions]
- print("XXX1", transaction_ids)
- # assert len(tags) == len(transactions)
-
- # (3) Find all ledger_transactionmetadata: assert two rows per tx
- tx_metadata_ids = thl_lm.get_tx_metadata_ids_by_txs(transactions=transactions)
- # assert len(tx_metadata) == len(transaction_ids)*2
-
- # (4) Find all ledger_entry: assert two rows per tx
- tx_entries = thl_lm.get_tx_entries_by_txs(transactions=transactions)
- tx_entry_ids = [tx_entry.id for tx_entry in tx_entries]
- # assert len(tx_entry) == len(transaction_ids)*2
-
- # (5) Delete records (all in 1 tx)
- with self.pg_config.make_connection() as conn:
- with conn.cursor() as c:
- # DELETE: tx_entry
- c.execute(
- """
- DELETE
- FROM ledger_entry
- WHERE transaction_id = ANY(%s)
- AND id = ANY(%s)
- """,
- [transaction_ids, tx_entry_ids],
- )
-
- # DELETE: tx_metadata
- c.execute(
- """
- DELETE
- FROM ledger_transactionmetadata
- WHERE transaction_id = ANY(%s)
- AND id = ANY(%s)
- """,
- [transaction_ids, list(tx_metadata_ids)],
+ if bpe.status == PayoutStatus.COMPLETE and all(
+ bp_pe.status == PayoutStatus.COMPLETE for bp_pe in bpe.bp_payouts
+ ):
+ try:
+ self.validate_business_payout_in_ledger(
+ ext_ref_id=ext_ref_id, thl_lm=thl_lm
)
-
- # DELETE: transactions
- c.execute(
- """
- DELETE
- FROM ledger_transaction
- WHERE id = ANY(%s)
- """,
- [transaction_ids],
+ except AssertionError as e:
+ LOG.error(
+ f"Business Payout Event {ext_ref_id} is COMPLETE but BP payouts are not in the ledger! {e}"
)
+ raise e
+ LOG.warning(
+ "Nothing to do! Business Payout is COMPLETE and all Brokerage Product payouts are also COMPLETE!"
+ )
+ return None
- # DELETE: event_payouts
- c.execute(
- """
- DELETE
- FROM event_payout
- WHERE ext_ref_id = %s
- AND uuid = ANY(%s)
- """,
- [ext_ref_id, event_payout_uuids],
+ for bp_pe in bpe.bp_payouts:
+ if bp_pe.status == PayoutStatus.PENDING:
+ product = pm.get_by_uuid(bp_pe.product_id)
+ self.retry_create_bp_payout_event_tx(
+ thl_ledger_manager=thl_lm, bp_pe=bp_pe, product=product
)
- conn.commit()
-
+ assert all(
+ bp_pe.status == PayoutStatus.COMPLETE for bp_pe in bpe.bp_payouts
+ ), "We created all BP payouts, but the statuses are not complete?"
+ self.validate_business_payout_in_ledger(ext_ref_id=ext_ref_id, thl_lm=thl_lm)
+ self.update_business_payout_event(pk=bpe.id, status=PayoutStatus.COMPLETE)
return None
def get_business_payout_events_for_products(
self,
- thl_ledger_manager: ThlLedgerManager,
product_uuids: Collection[UUIDStr],
order_by: OrderBy | None = OrderBy.ASC,
) -> list[BusinessPayoutEvent]:
res = self.get_bp_bp_payout_events_for_products(
- thl_ledger_manager=thl_ledger_manager,
product_uuids=product_uuids,
order_by=order_by,
)
@@ -1101,16 +1004,6 @@ class BusinessPayoutEventManager(BrokerageProductPayoutEventManager):
specific Business that was paid out and how much. It then determines
how to distribute the amount to each Brokerage Product in the
Business.
-
- :param business
- :param amount
- :param pm
- :param thl_lm: this must have rw permissions to add transactions to
- the ledger
- :param created
- :param transaction_id
-
- :return:
"""
assert business.balance is not None, (
"Must provide a full version of a Business in order to calculate"
@@ -1224,6 +1117,11 @@ class BusinessPayoutEventManager(BrokerageProductPayoutEventManager):
skip_wallet_balance_check=True,
)
+ self.update_business_payout_event(pk=bpe.id, status=PayoutStatus.COMPLETE)
+
+ return bpe
+
+ def update_business_payout_event(self, pk: int, status: PayoutStatus):
with self.connection() as conn:
with conn.cursor() as c:
c.execute(
@@ -1231,10 +1129,11 @@ class BusinessPayoutEventManager(BrokerageProductPayoutEventManager):
UPDATE supplier_payout
SET status = %(status)s
WHERE id = %(pk)s""",
- {"pk": bpe.id, "status": PayoutStatus.COMPLETE},
+ {"pk": pk, "status": status},
)
-
- return bpe
+ assert c.rowcount == 1, f"{id=} not found"
+ conn.commit()
+ return None
# import duckdb
diff --git a/generalresearch/models/gr/business.py b/generalresearch/models/gr/business.py
index cbb4bcb..bd66fbf 100644
--- a/generalresearch/models/gr/business.py
+++ b/generalresearch/models/gr/business.py
@@ -443,7 +443,6 @@ class Business(BaseModel):
self.prefetch_products(thl_pg_config=thl_pg_config)
self.payouts = bpem.get_business_payout_events_for_products(
- thl_ledger_manager=thl_lm,
product_uuids=self.product_uuids,
order_by=OrderBy.DESC,
)
diff --git a/generalresearch/models/thl/payout.py b/generalresearch/models/thl/payout.py
index 3c89e3b..2ecc232 100644
--- a/generalresearch/models/thl/payout.py
+++ b/generalresearch/models/thl/payout.py
@@ -18,7 +18,7 @@ from pydantic.json_schema import SkipJsonSchema
from typing_extensions import Self
from generalresearch.currency import USDCent
-from generalresearch.models.custom_types import AwareDatetimeISO, UUIDStr
+from generalresearch.models.custom_types import AwareDatetimeISO, UUIDStr, UUIDStrCoerce
from generalresearch.models.thl.definitions import PayoutStatus
from generalresearch.models.thl.ledger import OrderBy
from generalresearch.models.thl.wallet import PayoutType
@@ -42,20 +42,20 @@ class PayoutEvent(BaseModel):
multiple BrokerageProductPayoutEvents.
"""
- uuid: UUIDStr = Field(
+ uuid: UUIDStrCoerce = Field(
title="Payout Event Unique Identifier",
default_factory=lambda: uuid4().hex,
examples=["9453cd076713426cb68d05591c7145aa"],
)
- debit_account_uuid: UUIDStr | None = Field(
+ debit_account_uuid: UUIDStrCoerce | None = Field(
description="The LedgerAccount.uuid that money is being requested from. "
"Thie User or Brokerage Product is retrievable through the "
"LedgerAccount.reference_uuid",
examples=["18298cb1583846fbb06e4747b5310693"],
)
- cashout_method_uuid: UUIDStr | None = Field(
+ cashout_method_uuid: UUIDStrCoerce | None = Field(
description="References a row in the account_cashoutmethod table. This "
"is the cashout method that was used to request this "
"payout. (A cashout is the same thing as a payout)",
@@ -198,7 +198,7 @@ class BrokerageProductPayoutEvent(PayoutEvent):
- created: When the Brokerage Product was paid out
"""
- product_id: UUIDStr = Field(
+ product_id: UUIDStrCoerce = Field(
description="The Brokerage Product that was paid out",
examples=["1108d053e4fa47c5b0dbdcd03a7981e7"],
)
@@ -222,60 +222,6 @@ class BrokerageProductPayoutEvent(PayoutEvent):
def amount_usd_str(self) -> str:
return self.amount_usd.to_usd_str()
- # --- ORM ---
-
- @classmethod
- def from_payout_event(
- cls,
- pe: PayoutEvent,
- account_product_mapping: dict[UUIDStr, UUIDStr] | None = None,
- redis_config: RedisConfig | None = None,
- ) -> Self:
- # TODO!: prevent re-assignment, rework this...
-
- if account_product_mapping is None:
- rc = redis_config.create_redis_client()
- account_product_mapping: dict = rc.hgetall(name="pem:account_to_product")
- assert isinstance(account_product_mapping, dict)
- assert pe.uuid in account_product_mapping.keys()
-
- d = pe.model_dump()
- d["product_id"] = account_product_mapping[pe.debit_account_uuid]
- return cls.model_validate(d)
-
- @classmethod
- def from_payout_events(
- cls,
- payout_events: Collection[PayoutEvent],
- order_by=OrderBy,
- account_product_mapping: dict[UUIDStr, UUIDStr] | None = None,
- redis_config: RedisConfig | None = None,
- ) -> list[Self]:
- # TODO!: prevent re-assignment, rework this...
-
- if account_product_mapping is None:
- rc = redis_config.create_redis_client()
- account_product_mapping: dict = rc.hgetall(name="pem:account_to_product")
- assert isinstance(account_product_mapping, dict)
-
- res = []
- for pe in payout_events:
- res.append(
- cls.from_payout_event(
- pe=pe, account_product_mapping=account_product_mapping
- )
- )
-
- match order_by:
- case OrderBy.ASC:
- sorted_list = sorted(res, key=lambda x: x.created, reverse=False)
- case OrderBy.DESC:
- sorted_list = sorted(res, key=lambda x: x.created, reverse=True)
- case _:
- raise ValueError("Invalid order provided..")
-
- return sorted_list
-
class BusinessPayoutEvent(BaseModel):
"""A single payout event to a supplier Business."""
diff --git a/generalresearch/models/thl/product.py b/generalresearch/models/thl/product.py
index 3f21d92..9b7d66a 100644
--- a/generalresearch/models/thl/product.py
+++ b/generalresearch/models/thl/product.py
@@ -1207,7 +1207,6 @@ class Product(BaseModel, validate_assignment=True):
from generalresearch.models.thl.ledger import OrderBy
self.payouts = bp_pem.get_bp_bp_payout_events_for_products(
- thl_ledger_manager=thl_lm,
product_uuids=[self.uuid],
order_by=OrderBy.DESC,
)
diff --git a/tests/managers/thl/test_payout.py b/tests/managers/thl/test_payout.py
index f118c71..d586ba2 100644
--- a/tests/managers/thl/test_payout.py
+++ b/tests/managers/thl/test_payout.py
@@ -9,7 +9,8 @@ import io
import pandas as pd
import pytest
-
+from generalresearch.models.thl.product import Product
+from generalresearch.models.thl.user import User
from generalresearch.currency import USDCent
from generalresearch.managers.thl.ledger_manager.exceptions import (
LedgerTransactionConditionFailedError,
@@ -363,8 +364,6 @@ class TestBusinessPayoutEventManager:
delete_ledger_db()
create_main_accounts()
- from generalresearch.models.thl.product import Product
-
p1: Product = product_factory(business=business)
thl_lm.get_account_or_create_bp_wallet(product=p1)
business_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm)
@@ -446,9 +445,6 @@ class TestBusinessPayoutEventManager:
create_main_accounts()
delete_df_collection(coll=ledger_collection)
- from generalresearch.models.thl.product import Product
- from generalresearch.models.thl.user import User
-
p1: Product = product_factory(business=business)
u1: User = user_factory(product=p1)
thl_lm.get_account_or_create_bp_wallet(product=p1)
@@ -532,9 +528,6 @@ class TestBusinessPayoutEventManager:
create_main_accounts()
delete_df_collection(coll=ledger_collection)
- from generalresearch.models.thl.product import Product
- from generalresearch.models.thl.user import User
-
p1: Product = product_factory(business=business)
u1: User = user_factory(product=p1)
thl_lm.get_account_or_create_bp_wallet(product=p1)
@@ -762,9 +755,6 @@ class TestBusinessPayoutEventManager:
create_main_accounts()
delete_df_collection(coll=ledger_collection)
- from generalresearch.models.thl.product import Product
- from generalresearch.models.thl.user import User
-
p1: Product = product_factory(business=business)
u1: User = user_factory(product=p1)
thl_lm.get_account_or_create_bp_wallet(product=p1)
@@ -827,6 +817,114 @@ class TestBusinessPayoutEventManager:
),
]
+ def test_create_from_ach_or_wire(
+ self,
+ product,
+ mnt_filepath,
+ thl_lm,
+ client_no_amm,
+ thl_redis_config,
+ payout_event_manager,
+ brokerage_product_payout_event_manager,
+ business_payout_event_manager,
+ delete_ledger_db,
+ create_main_accounts,
+ delete_df_collection,
+ ledger_collection,
+ business,
+ user_factory,
+ product_factory,
+ session_with_tx_factory,
+ pop_ledger_merge,
+ start,
+ bp_payout_factory,
+ adj_to_fail_with_tx_factory,
+ thl_web_rr,
+ lm,
+ product_manager,
+ rm_ledger_collection,
+ rm_pop_ledger_merge,
+ caplog,
+ ):
+ """Test having a Business with three products"""
+ # Now let's load it up and actually test some things
+ delete_ledger_db()
+ create_main_accounts()
+ delete_df_collection(coll=ledger_collection)
+
+ p1: Product = product_factory(business=business)
+ p2: Product = product_factory(business=business)
+ p3: Product = product_factory(business=business)
+ u1: User = user_factory(product=p1)
+ u2: User = user_factory(product=p2)
+ u3: User = user_factory(product=p3)
+ thl_lm.get_account_or_create_bp_wallet(product=p1)
+ thl_lm.get_account_or_create_bp_wallet(product=p2)
+ thl_lm.get_account_or_create_bp_wallet(product=p3)
+
+ ach_id1 = uuid4().hex
+ ach_id2 = uuid4().hex
+
+ # Product 1: Complete, Payout, Recon..
+ s1 = session_with_tx_factory(
+ user=u1,
+ wall_req_cpi=Decimal("5.00"),
+ started=start + timedelta(days=1),
+ )
+
+ # Product 2: Complete x10
+ for idx in range(15):
+ session_with_tx_factory(
+ user=u2,
+ wall_req_cpi=Decimal("7.50"),
+ started=start + timedelta(days=1, hours=2, minutes=1 + idx),
+ )
+
+ # Product 3: Complete x5
+ for idx in range(10):
+ session_with_tx_factory(
+ user=u3,
+ wall_req_cpi=Decimal("7.50"),
+ started=start + timedelta(days=1, hours=3, minutes=1 + idx),
+ )
+
+ ledger_collection.initial_load(client=None, sync=True)
+ pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection)
+ business.prebuild_balance(
+ thl_pg_config=thl_web_rr,
+ lm=lm,
+ ds=mnt_filepath,
+ client=client_no_amm,
+ pop_ledger=pop_ledger_merge,
+ )
+
+ bb = business.balance
+ assert bb.payout == 25 * 712 + 475 # $7.50 * .95% = $7.125 = $7.12
+ assert bb.net == (25 * 7.12 + 4.75) * 100
+
+ bp1 = business_payout_event_manager.create_from_ach_or_wire(
+ business=business,
+ amount=USDCent(bb.available_balance),
+ pm=product_manager,
+ thl_lm=thl_lm,
+ created=start + timedelta(days=1, hours=5),
+ transaction_id=ach_id1,
+ )
+ print(f"{bp1=}")
+ assert isinstance(bp1, BusinessPayoutEvent)
+ assert len(bp1.bp_payouts) == 3
+
+ with caplog.at_level(logging.WARNING):
+ business_payout_event_manager.resume_failed_business_payout(
+ ext_ref_id=ach_id1, thl_lm=thl_lm, pm=product_manager
+ )
+ assert "Nothing to do!" in caplog.text
+
+ bpe = business_payout_event_manager.get_by_ext_ref_id(ext_ref_id=ach_id1)
+
+ assert 1 == 0
+ return None
+
def test_ach_payment(
self,
product,
@@ -866,9 +964,6 @@ class TestBusinessPayoutEventManager:
create_main_accounts()
delete_df_collection(coll=ledger_collection)
- from generalresearch.models.thl.product import Product
- from generalresearch.models.thl.user import User
-
p1: Product = product_factory(business=business)
p2: Product = product_factory(business=business)
p3: Product = product_factory(business=business)
@@ -1072,9 +1167,6 @@ class TestBusinessPayoutEventManager:
create_main_accounts()
delete_df_collection(coll=ledger_collection)
- from generalresearch.models.thl.product import Product
- from generalresearch.models.thl.user import User
-
p1: Product = product_factory(business=business)
p2: Product = product_factory(business=business)
p3: Product = product_factory(business=business)
@@ -1204,9 +1296,6 @@ class TestBusinessPayoutEventManager:
create_main_accounts()
delete_df_collection(coll=ledger_collection)
- from generalresearch.models.thl.product import Product
- from generalresearch.models.thl.user import User
-
p1: Product = product_factory(business=business)
p2: Product = product_factory(business=business)
p3: Product = product_factory(business=business)