diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/managers/thl/test_payout.py | 472 | ||||
| -rw-r--r-- | tests/models/thl/test_payout.py | 113 |
2 files changed, 298 insertions, 287 deletions
diff --git a/tests/managers/thl/test_payout.py b/tests/managers/thl/test_payout.py index 31087b8..b79a209 100644 --- a/tests/managers/thl/test_payout.py +++ b/tests/managers/thl/test_payout.py @@ -5,10 +5,15 @@ from decimal import Decimal from random import choice as rand_choice, randint from typing import Optional from uuid import uuid4 +import io import pandas as pd import pytest +from generalresearch import pg_helper +from generalresearch.managers.thl.ledger_manager.thl_ledger import ThlLedgerManager +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, @@ -16,7 +21,10 @@ from generalresearch.managers.thl.ledger_manager.exceptions import ( from generalresearch.managers.thl.payout import UserPayoutEventManager from generalresearch.models.thl.definitions import PayoutStatus from generalresearch.models.thl.ledger import LedgerEntry, Direction -from generalresearch.models.thl.payout import BusinessPayoutEvent +from generalresearch.models.thl.payout import ( + BusinessPayoutEvent, + BrokerageProductPayoutEvent, +) from generalresearch.models.thl.payout import UserPayoutEvent from generalresearch.models.thl.wallet import PayoutType from generalresearch.models.thl.ledger import LedgerAccount @@ -99,82 +107,43 @@ class TestPayout: delete_ledger_db, create_main_accounts, ): - delete_ledger_db() - create_main_accounts() - from generalresearch.models.thl.ledger import LedgerAccount - - thl_lm.get_account_or_create_bp_wallet(product=product) - brokerage_product_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm) - - with pytest.raises(expected_exception=LedgerTransactionConditionFailedError): - # wallet balance failure - brokerage_product_payout_event_manager.create_bp_payout_event( - thl_ledger_manager=thl_lm, - product=product, - amount=USDCent(100), - skip_wallet_balance_check=False, - skip_one_per_day_check=False, - ) - - # (we don't have a special method for this) Put money in the BP's account - amount_cents = 100 - cash_account: LedgerAccount = thl_lm.get_account_cash() - bp_wallet: LedgerAccount = thl_lm.get_account_or_create_bp_wallet( - product=product - ) - - entries = [ - LedgerEntry( - direction=Direction.DEBIT, - account_uuid=cash_account.uuid, - amount=amount_cents, - ), - LedgerEntry( - direction=Direction.CREDIT, - account_uuid=bp_wallet.uuid, - amount=amount_cents, - ), - ] - - lm.create_tx(entries=entries) - assert 100 == lm.get_account_balance(account=bp_wallet) - - # Then run it again for $1.00 - brokerage_product_payout_event_manager.create_bp_payout_event( - thl_ledger_manager=thl_lm, - product=product, - amount=USDCent(100), - skip_wallet_balance_check=False, - skip_one_per_day_check=False, - ) - assert 0 == lm.get_account_balance(account=bp_wallet) - - # Run again should without balance check, should still fail due to day check - with pytest.raises(LedgerTransactionConditionFailedError): - brokerage_product_payout_event_manager.create_bp_payout_event( - thl_ledger_manager=thl_lm, - product=product, - amount=USDCent(100), - skip_wallet_balance_check=True, - skip_one_per_day_check=False, - ) + # create_bp_payout_event does not get called directly. We have tests + # for the ledger methods already + pass - # And then we can run again skip both checks - pe = brokerage_product_payout_event_manager.create_bp_payout_event( - thl_ledger_manager=thl_lm, - product=product, + @pytest.fixture + def pending_bp_pe( + self, + thl_web_rw, + product, + thl_lm: ThlLedgerManager, + brokerage_product_payout_event_manager, + utc_now + ) -> BrokerageProductPayoutEvent: + account = thl_lm.get_account_or_create_bp_wallet(product=product) + bp_pe = BrokerageProductPayoutEvent( + product_id=product.uuid, amount=USDCent(100), - skip_wallet_balance_check=True, - skip_one_per_day_check=True, - ) - assert -100 == lm.get_account_balance(account=bp_wallet) - - pe = brokerage_product_payout_event_manager.get_by_uuid(pe.uuid) - txs = lm.get_tx_filtered_by_metadata( - metadata_key="event_payout", metadata_value=pe.uuid - ) - - assert 1 == len(txs) + payout_type=PayoutType.ACH, + debit_account_uuid=account.uuid, + cashout_method_uuid=brokerage_product_payout_event_manager.CASHOUT_METHOD_UUID, + created=utc_now + ) + params = bp_pe.model_dump_postgres() + # This shouldn't exist. For testing only, so no supplier_payout + params['supplier_payout_id'] = None + thl_web_rw.execute_write(""" + INSERT INTO event_payout ( + uuid, debit_account_uuid, created, cashout_method_uuid, + amount, status, ext_ref_id, payout_type, order_data, + request_data, supplier_payout_id + ) VALUES ( + %(uuid)s, %(debit_account_uuid)s, %(created)s, %(cashout_method_uuid)s, + %(amount)s, %(status)s, %(ext_ref_id)s, %(payout_type)s, %(order_data)s, + %(request_data)s, %(supplier_payout_id)s + ); + """, params) + return bp_pe def test_create_bp_payout_quick_dupe( self, @@ -186,26 +155,22 @@ class TestPayout: lm, utc_now, create_main_accounts, + pending_bp_pe, ): thl_lm.get_account_or_create_bp_wallet(product=product) - brokerage_product_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm) - brokerage_product_payout_event_manager.create_bp_payout_event( + brokerage_product_payout_event_manager.create_tx_bp_payout_from_payout_event( thl_ledger_manager=thl_lm, + bp_pe=pending_bp_pe, product=product, - amount=USDCent(100), - skip_wallet_balance_check=True, - skip_one_per_day_check=True, created=utc_now, ) with pytest.raises(ValueError) as cm: - brokerage_product_payout_event_manager.create_bp_payout_event( + brokerage_product_payout_event_manager.create_tx_bp_payout_from_payout_event( thl_ledger_manager=thl_lm, product=product, - amount=USDCent(100), - skip_wallet_balance_check=True, - skip_one_per_day_check=True, + bp_pe=pending_bp_pe, created=utc_now, ) assert "Payout event already exists!" in str(cm.value) @@ -290,44 +255,7 @@ class TestPayout: class TestPayoutEventManager: - def test_set_account_lookup_table( - self, payout_event_manager, thl_redis_config, thl_lm, delete_ledger_db - ): - delete_ledger_db() - rc = thl_redis_config.create_redis_client() - rc.delete("pem:account_to_product") - rc.delete("pem:product_to_account") - N = 5 - - for idx in range(N): - thl_lm.get_account_or_create_bp_wallet_by_uuid(product_uuid=uuid4().hex) - - res = rc.hgetall(name="pem:account_to_product") - assert len(res.items()) == 0 - - res = rc.hgetall(name="pem:product_to_account") - assert len(res.items()) == 0 - - payout_event_manager.set_account_lookup_table( - thl_lm=thl_lm, - ) - - res = rc.hgetall(name="pem:account_to_product") - assert len(res.items()) == N - - res = rc.hgetall(name="pem:product_to_account") - assert len(res.items()) == N - - thl_lm.get_account_or_create_bp_wallet_by_uuid(product_uuid=uuid4().hex) - payout_event_manager.set_account_lookup_table( - thl_lm=thl_lm, - ) - - res = rc.hgetall(name="pem:account_to_product") - assert len(res.items()) == N + 1 - - res = rc.hgetall(name="pem:product_to_account") - assert len(res.items()) == N + 1 + pass class TestBusinessPayoutEventManager: @@ -359,50 +287,22 @@ 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) ach_id1 = uuid4().hex ach_id2 = uuid4().hex - bp_payout_factory( - product=p1, - amount=USDCent(1), - ext_ref_id=None, - skip_wallet_balance_check=True, - skip_one_per_day_check=True, - ) + # ext_ref_id is required now + bp_payout_factory(product=p1,amount=USDCent(1),ext_ref_id="none") - bp_payout_factory( - product=p1, - amount=USDCent(1), - ext_ref_id=ach_id1, - skip_wallet_balance_check=True, - skip_one_per_day_check=True, - ) + bp_payout_factory(product=p1,amount=USDCent(1),ext_ref_id=ach_id1) + with pytest.raises(expected_exception=ValueError, match="Cannot create a BusinessPayoutEvent with an existing transaction_id"): + bp_payout_factory(product=p1,amount=USDCent(25),ext_ref_id=ach_id1) - bp_payout_factory( - product=p1, - amount=USDCent(25), - ext_ref_id=ach_id1, - skip_wallet_balance_check=True, - skip_one_per_day_check=True, - ) - - bp_payout_factory( - product=p1, - amount=USDCent(50), - ext_ref_id=ach_id2, - skip_wallet_balance_check=True, - skip_one_per_day_check=True, - ) + bp_payout_factory(product=p1,amount=USDCent(50),ext_ref_id=ach_id2) business.prebuild_payouts( - thl_pg_config=thl_web_rr, - thl_lm=thl_lm, bpem=business_payout_event_manager, ) @@ -410,11 +310,14 @@ class TestBusinessPayoutEventManager: assert business.payouts_total == sum([pe.amount for pe in business.payouts]) assert business.payouts[0].created > business.payouts[1].created assert len(business.payouts[0].bp_payouts) == 1 - assert len(business.payouts[1].bp_payouts) == 2 + + # Cannot pay out the same product twice in the same business payout + # assert len(business.payouts[1].bp_payouts) == 2 + assert len(business.payouts[1].bp_payouts) == 1 assert business.payouts[0].ext_ref_id == ach_id2 assert business.payouts[1].ext_ref_id == ach_id1 - assert business.payouts[2].ext_ref_id is None + assert business.payouts[2].ext_ref_id == "none" def test_update_ext_reference_ids( self, @@ -442,13 +345,9 @@ 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) - business_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm) # $250.00 to work with for idx in range(1, 10): @@ -461,12 +360,11 @@ class TestBusinessPayoutEventManager: ach_id1 = uuid4().hex ach_id2 = uuid4().hex - with pytest.raises(expected_exception=Warning) as cm: + with pytest.raises(expected_exception=AssertionError, match="No Business Payout found"): business_payout_event_manager.update_ext_reference_ids( new_value=ach_id2, current_value=ach_id1, ) - assert "No event_payouts found to UPDATE" in str(cm) # We must build the balance to issue ACH/Wire ledger_collection.initial_load(client=None, sync=True) @@ -487,6 +385,7 @@ class TestBusinessPayoutEventManager: transaction_id=ach_id1, ) assert isinstance(res, BusinessPayoutEvent) + assert business_payout_event_manager.get_by_ext_ref_id(ext_ref_id=ach_id1) # Okay, now that there is a payout_event, let's try to update the # ext_reference_id @@ -495,108 +394,10 @@ class TestBusinessPayoutEventManager: current_value=ach_id1, ) - res = business_payout_event_manager.filter_by(ext_ref_id=ach_id1) - assert len(res) == 0 - - res = business_payout_event_manager.filter_by(ext_ref_id=ach_id2) - assert len(res) == 1 - - def test_delete_failed_business_payout( - self, - brokerage_product_payout_event_manager, - business_payout_event_manager, - delete_ledger_db, - create_main_accounts, - thl_lm, - thl_web_rr, - product_factory, - bp_payout_factory, - currency, - delete_df_collection, - user_factory, - ledger_collection, - session_with_tx_factory, - pop_ledger_merge, - client_no_amm, - mnt_filepath, - lm, - product_manager, - start, - business, - ): - delete_ledger_db() - 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) - business_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm) - - # $250.00 to work with - for idx in range(1, 10): - session_with_tx_factory( - user=u1, - wall_req_cpi=Decimal("25.00"), - started=start + timedelta(days=1, minutes=idx), - ) - - # We must build the balance to issue ACH/Wire - 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, - ) - - ach_id1 = uuid4().hex - - res = business_payout_event_manager.create_from_ach_or_wire( - business=business, - amount=USDCent(100_01), - pm=product_manager, - thl_lm=thl_lm, - transaction_id=ach_id1, - ) - assert isinstance(res, BusinessPayoutEvent) - - # (1) Confirm the initial Event Payout, Tx, TxMeta, TxEntry all exist - event_payouts = business_payout_event_manager.filter_by(ext_ref_id=ach_id1) - event_payout_uuids = [i.uuid for i in event_payouts] - assert len(event_payout_uuids) == 1 - tags = [f"{currency.value}:bp_payout:{x}" for x in event_payout_uuids] - transactions = thl_lm.get_txs_by_tags(tags=tags) - assert len(transactions) == 1 - tx_metadata_ids = thl_lm.get_tx_metadata_ids_by_txs(transactions=transactions) - assert len(tx_metadata_ids) == 2 - tx_entries = thl_lm.get_tx_entries_by_txs(transactions=transactions) - assert len(tx_entries) == 2 - - # (2) Delete! - business_payout_event_manager.delete_failed_business_payout( - ext_ref_id=ach_id1, thl_lm=thl_lm - ) - - # (3) Confirm the initial Event Payout, Tx, TxMeta, TxEntry have - # all been deleted - res = business_payout_event_manager.filter_by(ext_ref_id=ach_id1) - assert len(res) == 0 + with pytest.raises(expected_exception=AssertionError, match="No Business Payout found"): + business_payout_event_manager.get_by_ext_ref_id(ext_ref_id=ach_id1) - # Note: b/c the event_payout shouldn't exist anymore, we are taking - # the tag strings and transactions from when they did.. - res = thl_lm.get_txs_by_tags(tags=tags) - assert len(res) == 0 - - tx_metadata_ids = thl_lm.get_tx_metadata_ids_by_txs(transactions=transactions) - assert len(tx_metadata_ids) == 0 - tx_entries = thl_lm.get_tx_entries_by_txs(transactions=transactions) - assert len(tx_entries) == 0 + assert business_payout_event_manager.get_by_ext_ref_id(ext_ref_id=ach_id2) def test_recoup_empty(self, business_payout_event_manager): res = {uuid4().hex: USDCent(0) for i in range(100)} @@ -708,7 +509,6 @@ class TestBusinessPayoutEventManager: assert int(res.deduction.sum()) == 0 def test_distribute_amount(self, business_payout_event_manager): - import io df = pd.read_csv( io.StringIO( @@ -748,7 +548,7 @@ class TestBusinessPayoutEventManager: lm, product_manager, ): - """Test having a Business with three products.. one that lost money + """Test having a Business with three products. One that lost money and two that gained money. Ensure that the Business balance reflects that to compensate for the Product in the negative and only assigns Brokerage Product payments from the 2 accounts that have @@ -759,9 +559,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) @@ -776,7 +573,6 @@ class TestBusinessPayoutEventManager: wall_req_cpi=Decimal("5.00"), started=start + timedelta(days=6), ) - payout_event_manager.set_account_lookup_table(thl_lm=thl_lm) bp_payout_factory( product=u1.product, amount=USDCent(475), # 95% of $5.00 @@ -799,9 +595,131 @@ class TestBusinessPayoutEventManager: amount=USDCent(500), pm=product_manager, thl_lm=thl_lm, + transaction_id=uuid4().hex, ) assert "Must issue Supplier Payouts at least $100 minimum." in str(cm) + 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 $10 x 20 + for idx in range(20): + session_with_tx_factory( + user=u2, + wall_req_cpi=Decimal("10.00"), + started=start + timedelta(days=1, hours=2, minutes=1 + idx), + ) + + # Product 2: Complete $10 x 30 + for idx in range(30): + session_with_tx_factory( + user=u3, + wall_req_cpi=Decimal("10.00"), + 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 == 475_00 # $500 * .95% = $475 + assert bb.net == 475_00 + + bp1 = business_payout_event_manager.create_from_ach_or_wire( + business=business, + amount=USDCent(100_00), + 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) == 2 + + bp2 = 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=2, hours=5), + transaction_id=ach_id2, + ) + print(f"{bp2=}") + assert isinstance(bp2, BusinessPayoutEvent) + assert len(bp2.bp_payouts) == 2 + + 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) + # bp_pe = bpe.bp_payouts[0] + # thl_web_rr.execute_write( + # """ + # UPDATE event_payout + # SET status = %(status)s + # WHERE uuid = %(uuid)s""", + # {"uuid": bp_pe.uuid, "status": PayoutStatus.FAILED}, + # ) + + assert 1 == 0 + return None + def test_ach_payment( self, product, @@ -841,9 +759,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) @@ -863,7 +778,6 @@ class TestBusinessPayoutEventManager: wall_req_cpi=Decimal("5.00"), started=start + timedelta(days=1), ) - payout_event_manager.set_account_lookup_table(thl_lm=thl_lm) bp_payout_factory( product=u1.product, amount=USDCent(475), # 95% of $5.00 @@ -1047,9 +961,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) @@ -1081,8 +992,6 @@ class TestBusinessPayoutEventManager: pop_ledger=pop_ledger_merge, ) business.prebuild_payouts( - thl_pg_config=thl_web_rr, - thl_lm=thl_lm, bpem=business_payout_event_manager, ) @@ -1179,9 +1088,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) diff --git a/tests/models/thl/test_payout.py b/tests/models/thl/test_payout.py index 3a51328..7068a41 100644 --- a/tests/models/thl/test_payout.py +++ b/tests/models/thl/test_payout.py @@ -1,10 +1,115 @@ +from uuid import uuid4 + +import pytest +from pydantic import ValidationError + +from generalresearch.currency import USDCent +from generalresearch.models.gr import Team +from generalresearch.models.thl.payout import ( + BusinessPayoutEvent, + BrokerageProductPayoutEvent, +) +from generalresearch.models.thl.wallet import PayoutType + +from generalresearch.models.gr.business import Business, BusinessAddress, BusinessType + + class TestBusinessPayoutEvent: def test_validate(self): - from generalresearch.models.gr.business import Business - instance = Business.model_validate_json( - json_data='{"id":123,"uuid":"947f6ba5250d442b9a66cde9ee33605a","name":"Example » Demo","kind":"c","tax_number":null,"contact":null,"addresses":[],"teams":[{"id":53,"uuid":"8e4197dcaefe4f1f831a02b212e6b44a","name":"Example » Demo","memberships":null,"gr_users":null,"businesses":null,"products":null}],"products":[{"id":"fc23e741b5004581b30e6478363525df","id_int":1234,"name":"Example","enabled":true,"payments_enabled":true,"created":"2025-04-14T13:25:37.279403Z","team_id":"9e4197dcaefe4f1f831a02b212e6b44a","business_id":"857f6ba6160d442b9a66cde9ee33605a","tags":[],"commission_pct":"0.050000","redirect_url":"https://pam-api-us.reppublika.com/v2/public/4970ef00-0ef7-11f0-9962-05cb6323c84c/grl/status","harmonizer_domain":"https://talk.generalresearch.com/","sources_config":{"user_defined":[{"name":"w","active":false,"banned_countries":[],"allow_mobile_ip":true,"supplier_id":null,"allow_pii_only_buyers":false,"allow_unhashed_buyers":false,"withhold_profiling":false,"pass_unconditional_eligible_unknowns":true,"address":null,"allow_vpn":null,"distribute_harmonizer_active":null}]},"session_config":{"max_session_len":600,"max_session_hard_retry":5,"min_payout":"0.14"},"payout_config":{"payout_format":null,"payout_transformation":null},"user_wallet_config":{"enabled":false,"amt":false,"supported_payout_types":["CASH_IN_MAIL","PAYPAL","TANGO"],"min_cashout":null},"user_create_config":{"min_hourly_create_limit":0,"max_hourly_create_limit":null},"offerwall_config":{},"profiling_config":{"enabled":true,"grs_enabled":true,"n_questions":null,"max_questions":10,"avg_question_count":5.0,"task_injection_freq_mult":1.0,"non_us_mult":2.0,"hidden_questions_expiration_hours":168},"user_health_config":{"banned_countries":[],"allow_ban_iphist":true},"yield_man_config":{},"balance":null,"payouts_total_str":null,"payouts_total":null,"payouts":null,"user_wallet":{"enabled":false,"amt":false,"supported_payout_types":["CASH_IN_MAIL","PAYPAL","TANGO"],"min_cashout":null}}],"bank_accounts":[],"balance":{"product_balances":[{"product_id":"fc14e741b5004581b30e6478363414df","last_event":null,"bp_payment_credit":780251,"adjustment_credit":4678,"adjustment_debit":26446,"supplier_credit":0,"supplier_debit":451513,"user_bonus_credit":0,"user_bonus_debit":0,"issued_payment":0,"payout":780251,"payout_usd_str":"$7,802.51","adjustment":-21768,"expense":0,"net":758483,"payment":451513,"payment_usd_str":"$4,515.13","balance":306970,"retainer":76742,"retainer_usd_str":"$767.42","available_balance":230228,"available_balance_usd_str":"$2,302.28","recoup":0,"recoup_usd_str":"$0.00","adjustment_percent":0.027898714644390074}],"payout":780251,"payout_usd_str":"$7,802.51","adjustment":-21768,"expense":0,"net":758483,"net_usd_str":"$7,584.83","payment":451513,"payment_usd_str":"$4,515.13","balance":306970,"balance_usd_str":"$3,069.70","retainer":76742,"retainer_usd_str":"$767.42","available_balance":230228,"available_balance_usd_str":"$2,302.28","adjustment_percent":0.027898714644390074,"recoup":0,"recoup_usd_str":"$0.00"},"payouts_total_str":"$4,515.13","payouts_total":451513,"payouts":[{"bp_payouts":[{"uuid":"40cf2c3c341e4f9d985be4bca43e6116","debit_account_uuid":"3a058056da85493f9b7cdfe375aad0e0","cashout_method_uuid":"602113e330cf43ae85c07d94b5100291","created":"2025-08-02T09:18:20.433329Z","amount":345735,"status":"COMPLETE","ext_ref_id":null,"payout_type":"ACH","request_data":{},"order_data":null,"product_id":"fc14e741b5004581b30e6478363414df","method":"ACH","amount_usd":345735,"amount_usd_str":"$3,457.35"}],"amount":345735,"amount_usd_str":"$3,457.35","created":"2025-08-02T09:18:20.433329Z","line_items":1,"ext_ref_id":null},{"bp_payouts":[{"uuid":"63ce1787087248978919015c8fcd5ab9","debit_account_uuid":"3a058056da85493f9b7cdfe375aad0e0","cashout_method_uuid":"602113e330cf43ae85c07d94b5100291","created":"2025-06-10T22:16:18.765668Z","amount":105778,"status":"COMPLETE","ext_ref_id":"11175997868","payout_type":"ACH","request_data":{},"order_data":null,"product_id":"fc14e741b5004581b30e6478363414df","method":"ACH","amount_usd":105778,"amount_usd_str":"$1,057.78"}],"amount":105778,"amount_usd_str":"$1,057.78","created":"2025-06-10T22:16:18.765668Z","line_items":1,"ext_ref_id":"11175997868"}]}' + # Doesn't validate anymore + # instance = Business.model_validate_json( + # json_data='{"id":123,"uuid":"947f6ba5250d442b9a66cde9ee33605a","name":"Example » Demo","kind":"c","tax_number":null,"contact":null,"addresses":[],"teams":[{"id":53,"uuid":"8e4197dcaefe4f1f831a02b212e6b44a","name":"Example » Demo","memberships":null,"gr_users":null,"businesses":null,"products":null}],"products":[{"id":"fc23e741b5004581b30e6478363525df","id_int":1234,"name":"Example","enabled":true,"payments_enabled":true,"created":"2025-04-14T13:25:37.279403Z","team_id":"9e4197dcaefe4f1f831a02b212e6b44a","business_id":"857f6ba6160d442b9a66cde9ee33605a","tags":[],"commission_pct":"0.050000","redirect_url":"https://pam-api-us.reppublika.com/v2/public/4970ef00-0ef7-11f0-9962-05cb6323c84c/grl/status","harmonizer_domain":"https://talk.generalresearch.com/","sources_config":{"user_defined":[{"name":"w","active":false,"banned_countries":[],"allow_mobile_ip":true,"supplier_id":null,"allow_pii_only_buyers":false,"allow_unhashed_buyers":false,"withhold_profiling":false,"pass_unconditional_eligible_unknowns":true,"address":null,"allow_vpn":null,"distribute_harmonizer_active":null}]},"session_config":{"max_session_len":600,"max_session_hard_retry":5,"min_payout":"0.14"},"payout_config":{"payout_format":null,"payout_transformation":null},"user_wallet_config":{"enabled":false,"amt":false,"supported_payout_types":["CASH_IN_MAIL","PAYPAL","TANGO"],"min_cashout":null},"user_create_config":{"min_hourly_create_limit":0,"max_hourly_create_limit":null},"offerwall_config":{},"profiling_config":{"enabled":true,"grs_enabled":true,"n_questions":null,"max_questions":10,"avg_question_count":5.0,"task_injection_freq_mult":1.0,"non_us_mult":2.0,"hidden_questions_expiration_hours":168},"user_health_config":{"banned_countries":[],"allow_ban_iphist":true},"yield_man_config":{},"balance":null,"payouts_total_str":null,"payouts_total":null,"payouts":null,"user_wallet":{"enabled":false,"amt":false,"supported_payout_types":["CASH_IN_MAIL","PAYPAL","TANGO"],"min_cashout":null}}],"bank_accounts":[],"balance":{"product_balances":[{"product_id":"fc14e741b5004581b30e6478363414df","last_event":null,"bp_payment_credit":780251,"adjustment_credit":4678,"adjustment_debit":26446,"supplier_credit":0,"supplier_debit":451513,"user_bonus_credit":0,"user_bonus_debit":0,"issued_payment":0,"payout":780251,"payout_usd_str":"$7,802.51","adjustment":-21768,"expense":0,"net":758483,"payment":451513,"payment_usd_str":"$4,515.13","balance":306970,"retainer":76742,"retainer_usd_str":"$767.42","available_balance":230228,"available_balance_usd_str":"$2,302.28","recoup":0,"recoup_usd_str":"$0.00","adjustment_percent":0.027898714644390074}],"payout":780251,"payout_usd_str":"$7,802.51","adjustment":-21768,"expense":0,"net":758483,"net_usd_str":"$7,584.83","payment":451513,"payment_usd_str":"$4,515.13","balance":306970,"balance_usd_str":"$3,069.70","retainer":76742,"retainer_usd_str":"$767.42","available_balance":230228,"available_balance_usd_str":"$2,302.28","adjustment_percent":0.027898714644390074,"recoup":0,"recoup_usd_str":"$0.00"},"payouts_total_str":"$4,515.13","payouts_total":451513,"payouts":[{"bp_payouts":[{"uuid":"40cf2c3c341e4f9d985be4bca43e6116","debit_account_uuid":"3a058056da85493f9b7cdfe375aad0e0","cashout_method_uuid":"602113e330cf43ae85c07d94b5100291","created":"2025-08-02T09:18:20.433329Z","amount":345735,"status":"COMPLETE","ext_ref_id":null,"payout_type":"ACH","request_data":{},"order_data":null,"product_id":"fc14e741b5004581b30e6478363414df","method":"ACH","amount_usd":345735,"amount_usd_str":"$3,457.35"}],"amount":345735,"amount_usd_str":"$3,457.35","created":"2025-08-02T09:18:20.433329Z","line_items":1,"ext_ref_id":null},{"bp_payouts":[{"uuid":"63ce1787087248978919015c8fcd5ab9","debit_account_uuid":"3a058056da85493f9b7cdfe375aad0e0","cashout_method_uuid":"602113e330cf43ae85c07d94b5100291","created":"2025-06-10T22:16:18.765668Z","amount":105778,"status":"COMPLETE","ext_ref_id":"11175997868","payout_type":"ACH","request_data":{},"order_data":null,"product_id":"fc14e741b5004581b30e6478363414df","method":"ACH","amount_usd":105778,"amount_usd_str":"$1,057.78"}],"amount":105778,"amount_usd_str":"$1,057.78","created":"2025-06-10T22:16:18.765668Z","line_items":1,"ext_ref_id":"11175997868"}]}' + # ) + # assert isinstance(instance, Business) + + # Make manually + b = Business( + id=123, + uuid=uuid4().hex, + name="Example", + addresses=[ + BusinessAddress( + uuid=uuid4().hex, + city="xxx", + line_1="xxx", + state="fl", + business_id=123, + ) + ], + kind=BusinessType.COMPANY, + teams=[Team(uuid=uuid4().hex, name="Example » Demo")], + products=[], + bank_accounts=[], + ) + ext_ref_id = uuid4().hex + bpe = BusinessPayoutEvent( + business_id=uuid4().hex, + amount=USDCent(100_00), + payout_type=PayoutType.ACH, + ext_ref_id=ext_ref_id, ) + bpe.bp_payouts = [ + BrokerageProductPayoutEvent( + product_id=uuid4().hex, + payout_type=PayoutType.ACH, + amount=USDCent(47_00), + cashout_method_uuid=uuid4().hex, + debit_account_uuid=uuid4().hex, + ext_ref_id=ext_ref_id, + ), + BrokerageProductPayoutEvent( + product_id=uuid4().hex, + payout_type=PayoutType.ACH, + amount=USDCent(53_00), + cashout_method_uuid=uuid4().hex, + debit_account_uuid=uuid4().hex, + ext_ref_id=ext_ref_id, + ), + ] + + # Test validations (amount sum) + with pytest.raises( + ValidationError, + match="BusinessPayoutEvent.amount must equal the sum of bp_payouts amounts", + ): + bpe.bp_payouts = [ + BrokerageProductPayoutEvent( + product_id=uuid4().hex, + payout_type=PayoutType.ACH, + amount=USDCent(47_00), + cashout_method_uuid=uuid4().hex, + debit_account_uuid=uuid4().hex, + ext_ref_id=ext_ref_id, + ) + ] + + with pytest.raises( + ValidationError, + match="All BrokerageProductPayoutEvent.ext_ref_id values must equal", + ): + bpe.bp_payouts = [ + BrokerageProductPayoutEvent( + product_id=uuid4().hex, + payout_type=PayoutType.ACH, + amount=USDCent(100_00), + cashout_method_uuid=uuid4().hex, + debit_account_uuid=uuid4().hex, + ext_ref_id="a different value", + ) + ] - assert isinstance(instance, Business) + with pytest.raises( + ValidationError, match="All BrokerageProductPayoutEvent.payout_type values" + ): + bpe.bp_payouts = [ + BrokerageProductPayoutEvent( + product_id=uuid4().hex, + payout_type=PayoutType.PAYPAL, + amount=USDCent(100_00), + cashout_method_uuid=uuid4().hex, + debit_account_uuid=uuid4().hex, + ext_ref_id=ext_ref_id, + ) + ] |
