diff options
| author | Greg Stupp | 2026-09-18 22:39:18 +0000 |
|---|---|---|
| committer | Greg Stupp | 2026-09-18 22:39:18 +0000 |
| commit | b373b992cae3a9628a9cf64387c7b8e286d6e1e0 (patch) | |
| tree | 5e21759ea2c6925e8f54328fbbac88b6be007648 /tests/managers | |
| parent | 32f30b5ddc600e22e071f7659f1872a18ebf42c7 (diff) | |
| parent | e3c6a824f4176402e40217c5ce70f837dedc5c3d (diff) | |
| download | generalresearch-99239335cd649b2ee53c8dc09875872dad094b86.tar.gz generalresearch-99239335cd649b2ee53c8dc09875872dad094b86.zip | |
Merges pull request #4
v3.5.0
User Cashout Stuff pulled out of grpc, user ip history enrichments with grip (no maxmind)
Diffstat (limited to 'tests/managers')
| -rw-r--r-- | tests/managers/thl/test_cashout_method.py | 4 | ||||
| -rw-r--r-- | tests/managers/thl/test_ipinfo.py | 201 | ||||
| -rw-r--r-- | tests/managers/thl/test_ledger/test_lm_tx.py | 4 | ||||
| -rw-r--r-- | tests/managers/thl/test_ledger/test_lm_tx_locks.py | 21 | ||||
| -rw-r--r-- | tests/managers/thl/test_ledger/test_thl_lm_tx.py | 27 | ||||
| -rw-r--r-- | tests/managers/thl/test_ledger/test_thl_pem.py | 51 | ||||
| -rw-r--r-- | tests/managers/thl/test_payout.py | 49 | ||||
| -rw-r--r-- | tests/managers/thl/test_session_manager.py | 6 | ||||
| -rw-r--r-- | tests/managers/thl/test_survey_penalty.py | 2 | ||||
| -rw-r--r-- | tests/managers/thl/test_user_manager/test_base.py | 28 | ||||
| -rw-r--r-- | tests/managers/thl/test_user_manager/test_redis.py | 15 | ||||
| -rw-r--r-- | tests/managers/thl/test_userhealth.py | 187 | ||||
| -rw-r--r-- | tests/managers/thl/test_wallet/__init__.py | 0 | ||||
| -rw-r--r-- | tests/managers/thl/test_wallet/test_user_payout.py | 435 |
14 files changed, 642 insertions, 388 deletions
diff --git a/tests/managers/thl/test_cashout_method.py b/tests/managers/thl/test_cashout_method.py index fc364f2..bb978f8 100644 --- a/tests/managers/thl/test_cashout_method.py +++ b/tests/managers/thl/test_cashout_method.py @@ -13,7 +13,6 @@ from generalresearch.models.thl.wallet.cashout_method import ( from generalresearch.models.thl.wallet.definitions import PayoutType if TYPE_CHECKING: - from generalresearch.config import GRLBaseSettings from generalresearch.managers.thl.cashout_method import ( CashoutMethodManager, ) @@ -24,7 +23,6 @@ if TYPE_CHECKING: class TestTangoCashoutMethods: - def test_create_and_get( self, cashout_method_manager: CashoutMethodManager, @@ -51,9 +49,7 @@ class TestTangoCashoutMethods: assert len(res) == 2 - class TestUserCashoutMethods: - def test( self, cashout_method_manager: CashoutMethodManager, diff --git a/tests/managers/thl/test_ipinfo.py b/tests/managers/thl/test_ipinfo.py index c021eb9..d84cbf7 100644 --- a/tests/managers/thl/test_ipinfo.py +++ b/tests/managers/thl/test_ipinfo.py @@ -1,160 +1,47 @@ -from collections.abc import Callable -from typing import TYPE_CHECKING - -import faker - -from generalresearch.managers.thl.ipinfo import ( - GeoIpInfoManager, - IPGeonameManager, - IPInformationManager, -) -from generalresearch.models.thl.ipinfo import ( - GeoIPInformation, - IPGeoname, - IPInformation, -) - -if TYPE_CHECKING: - from generalresearch.pg_helper import PostgresConfig - from generalresearch.redis_helper import RedisConfig - -fake = faker.Faker() - - -class TestIPGeonameManager: - - def test_init( - self, thl_web_rr: PostgresConfig, ip_geoname_manager: IPGeonameManager - ): - - instance = IPGeonameManager(pg_config=thl_web_rr) - assert isinstance(instance, IPGeonameManager) - assert isinstance(ip_geoname_manager, IPGeonameManager) - - def test_create( - self, - ip_geoname_factory: Callable[..., IPGeoname], - ip_geoname_manager: IPGeonameManager, - ): - - instance = ip_geoname_factory() - assert isinstance(instance, IPGeoname) - - res = ip_geoname_manager.fetch_geoname_ids(filter_ids=[instance.geoname_id]) - assert res[0].model_dump_json() == instance.model_dump_json() - - -class TestIPInformationManager: - - def test_init( - self, thl_web_rr: PostgresConfig, ip_information_manager: IPInformationManager - ): - instance = IPInformationManager(pg_config=thl_web_rr) - assert isinstance(instance, IPInformationManager) - assert isinstance(ip_information_manager, IPInformationManager) - - def test_create( - self, - ip_information_factory: Callable[..., IPInformation], - ip_information_manager: IPInformationManager, - ): - instance = ip_information_factory() - assert isinstance(instance, IPInformation) - - res = ip_information_manager.fetch_ip_information(filter_ips=[instance.ip]) - assert res[0].model_dump_json() == instance.model_dump_json() - - def test_prefetch_geoname( - self, - ip_information: IPInformation, - ip_geoname: IPGeoname, - thl_web_rr: PostgresConfig, - ): - assert isinstance(ip_information, IPInformation) - - assert ip_information.geoname_id == ip_geoname.geoname_id - assert ip_information.geoname is None - - ip_information.prefetch_geoname(pg_config=thl_web_rr) - assert isinstance(ip_information.geoname, IPGeoname) +from generalresearch.managers.thl.ipinfo import GeoIpInfoManager +from generalresearch.models.thl.ipinfo import GeoIPInformation class TestGeoIpInfoManager: - def test_init( - self, - thl_web_rr: PostgresConfig, - thl_redis_config: RedisConfig, - geoipinfo_manager: GeoIpInfoManager, - ): - instance = GeoIpInfoManager(pg_config=thl_web_rr, redis_config=thl_redis_config) - assert isinstance(instance, GeoIpInfoManager) - assert isinstance(geoipinfo_manager, GeoIpInfoManager) - - def test_multi( - self, - ip_information_factory: Callable[..., IPInformation], - ip_geoname: IPGeoname, - geoipinfo_manager: GeoIpInfoManager, - ): - ip = fake.ipv4_public() - ip_information_factory(ip=ip, geoname=ip_geoname) - ips = [ip] - - # This only looks up in redis. They don't exist yet - res = geoipinfo_manager.get_cache_multi(ip_addresses=ips) - assert res == {ip: None} - - # Looks up in redis, if not exists, looks in mysql, then sets - # the caches that didn't exist. - res = geoipinfo_manager.get_multi(ip_addresses=ips) - assert res[ip] is not None - - ip2 = fake.ipv4_public() - ip_information_factory(ip=ip2, geoname=ip_geoname) - ips = [ip, ip2] - res = geoipinfo_manager.get_cache_multi(ip_addresses=ips) - assert res[ip] is not None - assert res[ip2] is None - res = geoipinfo_manager.get_multi(ip_addresses=ips) - assert res[ip] is not None - assert res[ip2] is not None - res = geoipinfo_manager.get_cache_multi(ip_addresses=ips) - assert res[ip] is not None - assert res[ip2] is not None - - def test_multi_ipv6( - self, - ip_information_factory: Callable[..., IPInformation], - ip_geoname: IPGeoname, - geoipinfo_manager: GeoIpInfoManager, - ): - ip = fake.ipv6() - # Make another IP that will be in the same /64 block. - ip2 = ip[:-1] + "a" if ip[-1] != "a" else ip[:-1] + "b" - ip_information_factory(ip=ip, geoname=ip_geoname) - ips = [ip, ip2] - print(f"{ips=}") - - # This only looks up in redis. They don't exist yet - res = geoipinfo_manager.get_cache_multi(ip_addresses=ips) - assert res == {ip: None, ip2: None} - - # Looks up in redis, if not exists, looks in mysql, then sets - # the caches that didn't exist. - res = geoipinfo_manager.get_multi(ip_addresses=ips) - - res1 = res[ip] - assert isinstance(res1, GeoIPInformation) - assert res1.ip == ip - assert res1.lookup_prefix == "/64" - - res2 = res[ip2] - assert isinstance(res2, GeoIPInformation) - assert res2.ip == ip2 - assert res2.lookup_prefix == "/64" - # they should be the same basically, except for the ip - - def test_doesnt_exist(self, geoipinfo_manager: GeoIpInfoManager): - ip = fake.ipv4_public() - res = geoipinfo_manager.get_multi(ip_addresses=[ip]) - assert res == {ip: None} + def test_get(self, geoip_info_manager: GeoIpInfoManager): + result = geoip_info_manager.get("8.8.8.8") + + assert result == GeoIPInformation( + ip="8.8.8.8", + country_iso="us", + is_anonymous=False, + autonomous_system_number=15169, + autonomous_system_organization="Google", + access_type=None, + ) + geoip_info_manager.grip_mmdb.lookup.assert_called_once_with("8.8.8.8") + + def test_get_multi(self, geoip_info_manager: GeoIpInfoManager): + result = geoip_info_manager.get_multi(["8.8.8.8", "1.1.1.1", "8.8.8.8"]) + + assert result == { + "8.8.8.8": GeoIPInformation( + ip="8.8.8.8", + country_iso="us", + is_anonymous=False, + autonomous_system_number=15169, + autonomous_system_organization="Google", + access_type=None, + ), + "1.1.1.1": GeoIPInformation( + ip="1.1.1.1", + country_iso="au", + is_anonymous=True, + autonomous_system_number=13335, + autonomous_system_organization="Cloudflare", + access_type=None, + ), + } + assert geoip_info_manager.grip_mmdb.lookup.call_count == 2 + assert { + call.args[0] for call in geoip_info_manager.grip_mmdb.lookup.call_args_list + } == {"8.8.8.8", "1.1.1.1"} + + def test_get_multi_empty(self, geoip_info_manager: GeoIpInfoManager): + assert geoip_info_manager.get_multi([]) == {} + geoip_info_manager.grip_mmdb.lookup.assert_not_called() diff --git a/tests/managers/thl/test_ledger/test_lm_tx.py b/tests/managers/thl/test_ledger/test_lm_tx.py index 025f6ac..d98b838 100644 --- a/tests/managers/thl/test_ledger/test_lm_tx.py +++ b/tests/managers/thl/test_ledger/test_lm_tx.py @@ -206,12 +206,12 @@ class TestLedgerManagerGetTx: self, ledger_tx: LedgerTransaction, tag: str, ledger_manager: LedgerManager ): # (1) search for a random tag - res = ledger_manager.get_tx_by_tag(tag="aaa:bbb") + res = ledger_manager.get_txs_by_tag(tag="aaa:bbb") assert isinstance(res, list) assert len(res) == 0 # (2) search for the tag that was used during ledger_transaction creation - res = ledger_manager.get_tx_by_tag(tag=tag) + res = ledger_manager.get_txs_by_tag(tag=tag) assert isinstance(res, list) assert len(res) == 1 diff --git a/tests/managers/thl/test_ledger/test_lm_tx_locks.py b/tests/managers/thl/test_ledger/test_lm_tx_locks.py index 166598e..91d9cbc 100644 --- a/tests/managers/thl/test_ledger/test_lm_tx_locks.py +++ b/tests/managers/thl/test_ledger/test_lm_tx_locks.py @@ -38,7 +38,6 @@ logger = logging.getLogger("LedgerManager") class TestLedgerLocks: - def test_a( self, user_factory: Callable[..., User], @@ -108,16 +107,16 @@ class TestLedgerLocks: f1 = generate_condition_mp_payment(wall=w1) f2 = generate_condition_mp_payment(wall=w2) f3 = generate_condition_mp_payment(wall=w3) - assert f1(ledger_manager) is False - assert f2(lm=ledger_manager) is False - assert f3(lm=ledger_manager) is True + assert f1(ledger_manager) == (False, "duplicate tag") + assert f2(lm=ledger_manager) == (False, "duplicate tag") + assert f3(lm=ledger_manager)[0] == True condition = f3 create_tx_func = lambda: thl_ledger_manager.create_tx_task_complete_( wall=w3, user=user ) assert isinstance(create_tx_func, Callable) - assert f3(ledger_manager) is True + assert f3(ledger_manager)[0] is True ledger_manager.redis_client.delete(flag_name) ledger_manager.redis_client.delete(lock_name) @@ -125,13 +124,14 @@ class TestLedgerLocks: tx = thl_ledger_manager.create_tx_protected( lock_key=lock_key, condition=condition, create_tx_func=create_tx_func ) - assert f3(ledger_manager) is False + assert f3(ledger_manager) == (False, "duplicate tag") # purposely hold the lock open tx = None ledger_manager.redis_client.set(lock_name, "1") - with caplog.at_level(logging.ERROR), pytest.raises( - expected_exception=LedgerTransactionCreateLockError + with ( + caplog.at_level(logging.ERROR), + pytest.raises(expected_exception=LedgerTransactionCreateLockError), ): tx = thl_ledger_manager.create_tx_protected( lock_key=lock_key, @@ -231,8 +231,9 @@ class TestLedgerLocks: # Purposely hold the lock open ledger_manager.redis_client.set(name=lock_name, value="1") - with caplog.at_level(logging.DEBUG), pytest.raises( - expected_exception=LedgerTransactionCreateLockError + with ( + caplog.at_level(logging.DEBUG), + pytest.raises(expected_exception=LedgerTransactionCreateLockError), ): tx = thl_ledger_manager.create_tx_task_complete( wall=wall3, user=user, created=wall3.started diff --git a/tests/managers/thl/test_ledger/test_thl_lm_tx.py b/tests/managers/thl/test_ledger/test_thl_lm_tx.py index 96ea0d6..22a7b73 100644 --- a/tests/managers/thl/test_ledger/test_thl_lm_tx.py +++ b/tests/managers/thl/test_ledger/test_thl_lm_tx.py @@ -806,6 +806,24 @@ class TestThlLedgerTxManager: assert thl_ledger_manager.get_session_attempt_credit(session.uuid) == 5 assert thl_ledger_manager.get_user_attempt_credit_balance(user) == 5 + attempt_credit_account = ( + thl_ledger_manager.get_account_or_create_user_attempt_credit(user) + ) + user_txs = thl_ledger_manager.get_user_txs( + user, account_uuid=attempt_credit_account.uuid + ) + assert user_txs.total == 1 + assert user_txs.transactions[0].tx_type == TransactionType.USER_ATTEMPT_CREDIT + assert user_txs.transactions[0].amount == 5 + assert user_txs.transactions[0].balance_after == 5 + assert user_txs.summary.user_attempt_credit.entry_count == 1 + assert user_txs.summary.user_attempt_credit.total_amount == 5 + + user_txs_by_name = thl_ledger_manager.get_user_txs( + user, qualified_name=attempt_credit_account.qualified_name + ) + assert user_txs_by_name == user_txs + with pytest.raises( LedgerTransactionConditionFailedError, match=r"^duplicate tag$" ): @@ -887,6 +905,15 @@ class TestThlLedgerTxManager: assert thl_ledger_manager.get_user_wallet_balance(user) == round(user_pay * 100) assert thl_ledger_manager.check_ledger_balanced() + attempt_credit_account = ( + thl_ledger_manager.get_account_or_create_user_attempt_credit(user) + ) + user_txs = thl_ledger_manager.get_user_txs( + user, account_uuid=attempt_credit_account.uuid + ) + assert [tx.amount for tx in user_txs.transactions] == [5, -5] + assert [tx.balance_after for tx in user_txs.transactions] == [5, 0] + def test_bp_payment_with_zero_attempt_credit_balance( self, session_factory: Callable[..., Session], diff --git a/tests/managers/thl/test_ledger/test_thl_pem.py b/tests/managers/thl/test_ledger/test_thl_pem.py index fb35aa4..348a02d 100644 --- a/tests/managers/thl/test_ledger/test_thl_pem.py +++ b/tests/managers/thl/test_ledger/test_thl_pem.py @@ -8,13 +8,9 @@ from uuid import UUID, uuid4 import pytest from generalresearch.currency import USDCent -from generalresearch.models.thl.definitions import PayoutStatus from generalresearch.models.thl.payout import ( BrokerageProductPayoutEvent, ) -from generalresearch.models.thl.wallet.cashout_method import ( - CashoutRequestInfo, -) if TYPE_CHECKING: from generalresearch.managers.thl.ledger_manager.ledger import LedgerManager @@ -23,6 +19,7 @@ if TYPE_CHECKING: ) from generalresearch.managers.thl.payout import ( BrokerageProductPayoutEventManager, + BusinessPayoutEventManager, UserPayoutEventManager, ) from generalresearch.models.thl.payout import UserPayoutEvent @@ -30,7 +27,6 @@ if TYPE_CHECKING: class TestThlPayoutEventManager: - def test_get_by_uuid( self, brokerage_product_payout_event_manager: BrokerageProductPayoutEventManager ): @@ -50,8 +46,9 @@ class TestThlPayoutEventManager: bp_payout_event_factory: Callable[..., BrokerageProductPayoutEvent], thl_ledger_manager: ThlLedgerManager, brokerage_product_payout_event_manager: BrokerageProductPayoutEventManager, + create_main_accounts, ): - + create_main_accounts() N_PRODUCTS = randint(3, 10) N_PAYOUT_EVENTS = randint(3, 10) amounts = [] @@ -122,38 +119,6 @@ class TestThlPayoutEventManager: assert len(res) == (N_PRODUCTS * N_PAYOUT_EVENTS) assert sum([i.amount for i in res]) == sum(amounts) - @pytest.mark.skip - def test_get_payout_detail(self, user_payout_event_manager: UserPayoutEventManager): - """This fails because the description coming back is None, but then - it tries to return a PayoutEvent which validates that the - description can't be None - """ - from generalresearch.models.thl.payout import ( - PayoutType, - ) - - rand_amount = randint(a=99, b=999) - - pe = user_payout_event_manager.create( - debit_account_uuid=uuid4().hex, - account_reference_type="str-type-random", - account_reference_uuid=uuid4().hex, - cashout_method_uuid=uuid4().hex, - description="Best payout !", - amount=rand_amount, - status=PayoutStatus.PENDING, - ext_ref_id="123", - payout_type=PayoutType.CASH_IN_MAIL, - request_data={"foo": 123}, - order_data={}, - ) - - res = user_payout_event_manager.get_payout_detail(pe_uuid=pe.uuid) - assert isinstance(res, CashoutRequestInfo) - - # def test_filter_by(self): - # raise NotImplementedError - def test_create( self, user_payout_event_factory: Callable[..., UserPayoutEvent], @@ -185,6 +150,7 @@ class TestThlPayoutEventManager: create_main_accounts: Callable[..., None], thl_ledger_manager: ThlLedgerManager, brokerage_product_payout_event_manager: BrokerageProductPayoutEventManager, + business_payout_event_manager: BusinessPayoutEventManager, ledger_manager: LedgerManager, ): @@ -198,13 +164,13 @@ class TestThlPayoutEventManager: # Save a Brokerage Product Payout, so we have something in the # Payout Event table and the respective ledger TX and Entry rows for it - pe = brokerage_product_payout_event_manager.create_bp_payout_event( + bus_pe = business_payout_event_manager.create_bp_payout_event( thl_ledger_manager=thl_ledger_manager, product=product, amount=USDCent(rand_amount), - skip_wallet_balance_check=True, - skip_one_per_day_check=True, + ext_ref_id=uuid4().hex ) + pe = bus_pe.bp_payouts[0] assert isinstance(pe, BrokerageProductPayoutEvent) # Now try to query for it! @@ -215,7 +181,7 @@ class TestThlPayoutEventManager: res = thl_ledger_manager.get_tx_bp_payouts(account_uuids=[uuid4().hex]) assert len(res) == 0 - # Confirm it added to the users balance. The amount is negative because + # Confirm it added to the user's balance. The amount is negative because # money was sent to the Brokerage product: Product, but they didn't have # any activity that earned them money bal = ledger_manager.get_account_balance(account=account_bp_wallet) @@ -223,7 +189,6 @@ class TestThlPayoutEventManager: class TestBPPayoutEvent: - def test_get_bp_bp_payout_events_for_products( self, product_factory: Callable[..., Product], diff --git a/tests/managers/thl/test_payout.py b/tests/managers/thl/test_payout.py index 52bbbec..be121d8 100644 --- a/tests/managers/thl/test_payout.py +++ b/tests/managers/thl/test_payout.py @@ -34,9 +34,9 @@ if TYPE_CHECKING: BrokerageProductPayoutEventManager, BusinessPayoutEventManager, PayoutEventManager, - UserPayoutEventManager, ) from generalresearch.managers.thl.product import ProductManager + from generalresearch.managers.thl.wallet.user_payout import UserPayoutEventManager from generalresearch.models.gr.business import Business from generalresearch.models.thl.ledger import LedgerAccount from generalresearch.models.thl.payout import ( @@ -163,7 +163,9 @@ class TestPayout: thl_ledger_manager: ThlLedgerManager, utc_now: datetime, pending_bp_pe: BrokerageProductPayoutEvent, + create_main_accounts, ): + create_main_accounts() thl_ledger_manager.get_account_or_create_bp_wallet(product=product) brokerage_product_payout_event_manager.create_tx_bp_payout_from_payout_event( @@ -386,7 +388,7 @@ class TestBusinessPayoutEventManager: ledger_collection.initial_load(client=None, sync=True) pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection) gr_business.prebuild_balance( - thl_pg_config=thl_web_rr, + product_manager=product_manager, lm=thl_ledger_manager, ds=mnt_filepath, client=client_no_amm, @@ -470,6 +472,7 @@ class TestBusinessPayoutEventManager: self, business_payout_event_manager: BusinessPayoutEventManager, request ): # TODO: Generate this file at random + # @max file is not commited fp = os.path.join( request.config.rootpath, "data/pytest_recoup_proportional.csv" ) @@ -688,7 +691,7 @@ class TestBusinessPayoutEventManager: ledger_collection.initial_load(client=None, sync=True) pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection) gr_business.prebuild_balance( - thl_pg_config=thl_web_rr, + product_manager=product_manager, lm=ledger_manager, ds=mnt_filepath, client=client_no_amm, @@ -860,17 +863,18 @@ class TestBusinessPayoutEventManager: assert gr_business.payouts is None gr_business.prebuild_payouts( - thl_pg_config=thl_web_rr, - thl_lm=thl_ledger_manager, bpem=business_payout_event_manager, ) assert isinstance(gr_business.payouts, list) assert len(gr_business.payouts) == 1 assert gr_business.payouts[0].ext_ref_id == ach_id1 + ach_id2 = uuid4().hex + bp1 = business_payout_event_manager.create_from_ach_or_wire( business=gr_business, amount=USDCent(bb1.available_balance), + transaction_id=ach_id2, pm=product_manager, thl_lm=thl_ledger_manager, created=start + timedelta(days=1, hours=5), @@ -882,16 +886,12 @@ class TestBusinessPayoutEventManager: bp1_tx = brokerage_product_payout_event_manager.check_for_ledger_tx( thl_ledger_manager=thl_ledger_manager, payout_event=bp1.bp_payouts[0], - product_id=bp1.bp_payouts[0].product_id, - amount=bp1.bp_payouts[0].amount, ) assert bp1_tx bp2_tx = brokerage_product_payout_event_manager.check_for_ledger_tx( thl_ledger_manager=thl_ledger_manager, payout_event=bp1.bp_payouts[1], - product_id=bp1.bp_payouts[1].product_id, - amount=bp1.bp_payouts[1].amount, ) assert bp2_tx @@ -901,22 +901,22 @@ class TestBusinessPayoutEventManager: 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, + gr_business.prebuild_balance( + product_manager=product_manager, lm=ledger_manager, ds=mnt_filepath, client=client_no_amm, pop_ledger=pop_ledger_merge, ) - business.prebuild_payouts( + gr_business.prebuild_payouts( bpem=business_payout_event_manager, ) - assert isinstance(business.payouts, list) - assert len(business.payouts) == 2 - assert len(business.payouts[0].bp_payouts) == 2 - assert len(business.payouts[1].bp_payouts) == 1 + assert isinstance(gr_business.payouts, list) + assert len(gr_business.payouts) == 2 + assert len(gr_business.payouts[0].bp_payouts) == 2 + assert len(gr_business.payouts[1].bp_payouts) == 1 - bb2 = business.balance + bb2 = gr_business.balance # Okay os we have the balance before, and after the Business Payout # of bb1.available_balance worth.. @@ -993,13 +993,12 @@ class TestBusinessPayoutEventManager: wall_req_cpi=Decimal("50.00"), started=start + timedelta(days=1, hours=2, minutes=1 + idx), ) - payout_event_manager.set_account_lookup_table(thl_lm=thl_ledger_manager) # Now that we paid out the business: Business, let's confirm the updated balances ledger_collection.initial_load(client=None, sync=True) pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection) gr_business.prebuild_balance( - thl_pg_config=thl_web_rr, + product_manager=product_manager, lm=ledger_manager, ds=mnt_filepath, client=client_no_amm, @@ -1039,6 +1038,7 @@ class TestBusinessPayoutEventManager: pm=product_manager, thl_lm=thl_ledger_manager, created=start + timedelta(days=1, hours=3), + transaction_id=uuid4().hex, ) assert isinstance(bp1, BusinessPayoutEvent) assert len(bp1.bp_payouts) == 3 @@ -1054,7 +1054,7 @@ class TestBusinessPayoutEventManager: # balance.available_balance are reflective of having a $250 ACH/Wire # sent to the Business gr_business.prebuild_balance( - thl_pg_config=thl_web_rr, + product_manager=product_manager, lm=ledger_manager, ds=mnt_filepath, client=client_no_amm, @@ -1120,14 +1120,13 @@ class TestBusinessPayoutEventManager: wall_req_cpi=Decimal("7.50"), started=start + timedelta(days=1, hours=1 + iidx, minutes=1 + idx), ) - payout_event_manager.set_account_lookup_table(thl_lm=thl_ledger_manager) rm_ledger_collection() rm_pop_ledger_merge() ledger_collection.initial_load(client=None, sync=True) pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection) gr_business.prebuild_balance( - thl_pg_config=thl_web_rr, + product_manager=product_manager, lm=ledger_manager, ds=mnt_filepath, client=client_no_amm, @@ -1148,7 +1147,7 @@ class TestBusinessPayoutEventManager: ledger_collection.initial_load(client=None, sync=True) pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection) gr_business.prebuild_balance( - thl_pg_config=thl_web_rr, + product_manager=product_manager, lm=ledger_manager, ds=mnt_filepath, client=client_no_amm, @@ -1172,12 +1171,10 @@ class TestBusinessPayoutEventManager: ledger_collection.initial_load(client=None, sync=True) pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection) gr_business.prebuild_payouts( - thl_pg_config=thl_web_rr, - thl_lm=thl_ledger_manager, bpem=business_payout_event_manager, ) gr_business.prebuild_balance( - thl_pg_config=thl_web_rr, + product_manager=product_manager, lm=ledger_manager, ds=mnt_filepath, client=client_no_amm, diff --git a/tests/managers/thl/test_session_manager.py b/tests/managers/thl/test_session_manager.py index 60edcb9..4b122ec 100644 --- a/tests/managers/thl/test_session_manager.py +++ b/tests/managers/thl/test_session_manager.py @@ -117,6 +117,7 @@ class TestSessionManagerFilter: def test_team( self, product_factory: Callable[..., Product], + product_manager, user_factory: Callable[..., User], gr_team: Team, session_manager: SessionManager, @@ -129,7 +130,7 @@ class TestSessionManagerFilter: u = user_factory(product=p1) session_manager.create(started=utc_hour_ago, user=u, uuid_id=uuid4().hex) - gr_team.prefetch_products(thl_pg_config=thl_web_rr) + gr_team.prefetch_products(product_manager=product_manager) assert len(gr_team.product_uuids) == 1 res = session_manager.filter(product_uuids=gr_team.product_uuids) assert len(res) == 5 @@ -142,6 +143,7 @@ class TestSessionManagerFilter: session_manager: SessionManager, utc_hour_ago: datetime, thl_web_rr: PostgresConfig, + product_manager, ): p1 = product_factory(business=gr_business) @@ -149,7 +151,7 @@ class TestSessionManagerFilter: u = user_factory(product=p1) session_manager.create(started=utc_hour_ago, user=u, uuid_id=uuid4().hex) - gr_business.prefetch_products(thl_pg_config=thl_web_rr) + gr_business.prefetch_products(product_manager=product_manager) assert len(gr_business.product_uuids) == 1 res = session_manager.filter(product_uuids=gr_business.product_uuids) assert len(res) == 5 diff --git a/tests/managers/thl/test_survey_penalty.py b/tests/managers/thl/test_survey_penalty.py index 04f69d2..71cea16 100644 --- a/tests/managers/thl/test_survey_penalty.py +++ b/tests/managers/thl/test_survey_penalty.py @@ -103,7 +103,7 @@ class TestSurveyPenalty: assert res == {"t:a": 0.1, "t:b": 0.2, "u:b": 0.1} assert surveypenalty_manager.cache.currsize == 1 cached_key = tuple(list(next(iter(surveypenalty_manager.cache.keys())))[1:]) - assert cached_key == ("product_id", product_uuid, "team_id", team_id_random) + assert cached_key == (("product_id", product_uuid), ("team_id", team_id_random)) # Both don't exist, return nothing res = surveypenalty_manager.get_penalties_for( diff --git a/tests/managers/thl/test_user_manager/test_base.py b/tests/managers/thl/test_user_manager/test_base.py index 5d12052..2a7e879 100644 --- a/tests/managers/thl/test_user_manager/test_base.py +++ b/tests/managers/thl/test_user_manager/test_base.py @@ -56,43 +56,43 @@ class TestUserManager: def test_get_user_no_inmemory(self): self.user_manager.clear_user_inmemory_cache(self.user) - self.user_manager.get_user.__wrapped__.cache_clear() + self.user_manager.cache_clear() u = self.user_manager.get_user(user_id=self.user.user_id) # this should hit mysql assert u == self.user - cache_info = self.user_manager.get_user.__wrapped__.cache_info() - assert cache_info.hits == 0, cache_info - assert cache_info.misses == 1, cache_info + cache_info = self.user_manager.cache_info() + assert cache_info['hits'] == 0, cache_info + assert cache_info['misses'] == 1, cache_info # this should hit the lru cache u = self.user_manager.get_user(user_id=self.user.user_id) assert u == self.user - cache_info = self.user_manager.get_user.__wrapped__.cache_info() - assert cache_info.hits == 1, cache_info - assert cache_info.misses == 1, cache_info + cache_info = self.user_manager.cache_info() + assert cache_info['hits'] == 1, cache_info + assert cache_info['misses'] == 1, cache_info def test_get_user_with_inmemory(self): # user_manager = self.get_user_manager() self.user_manager.set_user_inmemory_cache(self.user) - self.user_manager.get_user.__wrapped__.cache_clear() + self.user_manager.cache_clear() u = self.user_manager.get_user(user_id=self.user.user_id) # this should hit inmemory cache assert u == self.user - cache_info = self.user_manager.get_user.__wrapped__.cache_info() - assert cache_info.hits == 0, cache_info - assert cache_info.misses == 1, cache_info + cache_info = self.user_manager.cache_info() + assert cache_info['hits'] == 0, cache_info + assert cache_info['misses'] == 1, cache_info # this should hit the lru cache u = self.user_manager.get_user(user_id=self.user.user_id) assert u == self.user - cache_info = self.user_manager.get_user.__wrapped__.cache_info() - assert cache_info.hits == 1, cache_info - assert cache_info.misses == 1, cache_info + cache_info = self.user_manager.cache_info() + assert cache_info['hits'] == 1, cache_info + assert cache_info['misses'] == 1, cache_info class TestBlockUserManager: diff --git a/tests/managers/thl/test_user_manager/test_redis.py b/tests/managers/thl/test_user_manager/test_redis.py index f6b59c9..89d0a7a 100644 --- a/tests/managers/thl/test_user_manager/test_redis.py +++ b/tests/managers/thl/test_user_manager/test_redis.py @@ -8,15 +8,14 @@ from generalresearch.managers.base import Permission from generalresearch.managers.thl.user_manager.redis_user_manager import ( RedisUserManager, ) +from generalresearch.redis_helper import RedisConfig if TYPE_CHECKING: - from generalresearch.config import GRLBaseSettings from generalresearch.models.thl.user import User from generalresearch.pg_helper import PostgresConfig class TestUserManagerRedis: - def test_get_notset(self, redis_user_manager: RedisUserManager, user: User): redis_user_manager.clear_user(user=user) assert redis_user_manager.get_user(user_id=user.user_id) is None @@ -48,10 +47,10 @@ class TestUserManagerRedis: def test_get_with_cache_prefix( self, - settings: GRLBaseSettings, user: User, thl_web_rw: PostgresConfig, thl_web_rr: PostgresConfig, + thl_redis_config: RedisConfig, ): """ Confirm the prefix functionality is working; we do this so it @@ -67,19 +66,19 @@ class TestUserManagerRedis: pg_config=thl_web_rw, pg_config_rr=thl_web_rr, sql_permissions=[Permission.UPDATE, Permission.CREATE], - redis=settings.redis, - redis_timeout=settings.redis_timeout, + redis=thl_redis_config.dsn, + redis_timeout=1, ) um2 = UserManager( pg_config=thl_web_rw, pg_config_rr=thl_web_rr, sql_permissions=[Permission.UPDATE, Permission.CREATE], - redis=settings.redis, - redis_timeout=settings.redis_timeout, + redis=thl_redis_config.dsn, + redis_timeout=1, cache_prefix="user-lookup-v2", ) - + user = user.to_user_ref() um1.get_or_create_user( product_id=user.product_id, product_user_id=user.product_user_id ) diff --git a/tests/managers/thl/test_userhealth.py b/tests/managers/thl/test_userhealth.py index 268b110..bd2152a 100644 --- a/tests/managers/thl/test_userhealth.py +++ b/tests/managers/thl/test_userhealth.py @@ -3,11 +3,13 @@ from __future__ import annotations from collections.abc import Callable from datetime import UTC, datetime from typing import TYPE_CHECKING +from unittest.mock import Mock from uuid import uuid4 import faker import pytest +from generalresearch.managers.thl.ipinfo import GeoIpInfoManager from generalresearch.managers.thl.userhealth import ( AuditLogManager, IPRecordManager, @@ -23,10 +25,6 @@ from generalresearch.models.thl.user_iphistory import ( from generalresearch.models.thl.userhealth import AuditLog, AuditLogLevel if TYPE_CHECKING: - from generalresearch.models.thl.ipinfo import ( - IPGeoname, - IPInformation, - ) from generalresearch.models.thl.product import Product from generalresearch.models.thl.user import User from generalresearch.pg_helper import PostgresConfig @@ -36,7 +34,6 @@ fake = faker.Faker() class TestAuditLog: - def test_init(self, thl_web_rr: PostgresConfig, audit_log_manager: AuditLogManager): alm = AuditLogManager(pg_config=thl_web_rr) @@ -56,7 +53,7 @@ class TestAuditLog: user_id=user.user_id, level=level, event_type=uuid4().hex ) assert isinstance(instance, AuditLog) - assert instance.id != 1 + assert instance.id != 0 def test_get_by_id(self, audit_log: AuditLog, audit_log_manager: AuditLogManager): @@ -225,14 +222,16 @@ class TestAuditLog: class TestIPRecordManager: - def test_init( self, thl_web_rr: PostgresConfig, thl_redis_config: RedisConfig, ip_record_manager: IPRecordManager, ): - instance = IPRecordManager(pg_config=thl_web_rr, redis_config=thl_redis_config) + instance = IPRecordManager( + pg_config=thl_web_rr, + redis_config=thl_redis_config, + ) assert isinstance(instance, IPRecordManager) assert isinstance(ip_record_manager, IPRecordManager) @@ -240,10 +239,11 @@ class TestIPRecordManager: self, ip_record_manager: IPRecordManager, user: User, - ip_information: IPInformation, ip_record_factory: Callable[..., IPRecord], ): - instance = ip_record_factory(user_id=user.user_id, ip=ip_information.ip) + ip = fake.ipv4_public() + + instance = ip_record_factory(user=user, ip=ip) assert isinstance(instance, IPRecord) assert isinstance(instance.forwarded_ips, list) @@ -260,40 +260,12 @@ class TestIPRecordManager: def test_prefetch_info( self, ip_record_factory: Callable[..., IPRecord], - ip_information_factory: Callable[..., IPInformation], - ip_geoname: IPGeoname, user: User, thl_web_rr: PostgresConfig, thl_redis_config: RedisConfig, ): - - ip = fake.ipv4_public() - ip_information_factory(ip=ip, geoname=ip_geoname) - ipr: IPRecord = ip_record_factory(user_id=user.user_id, ip=ip) - assert isinstance(ipr, IPRecord) - - assert ipr.information is None - assert len(ipr.forwarded_ip_records) >= 1 - assert isinstance(ipr.forwarded_ip_records, list) - fipr = ipr.forwarded_ip_records[0] - assert fipr.information is None - - ipr.prefetch_ipinfo( - pg_config=thl_web_rr, - redis_config=thl_redis_config, - include_forwarded=True, - ) - assert isinstance(ipr.information, GeoIPInformation) - assert ipr.information.ip == ipr.ip == ip - assert fipr.information is None, "the ipinfo doesn't exist in the db yet" - - ip_information_factory(ip=fipr.ip, geoname=ip_geoname) - ipr.prefetch_ipinfo( - pg_config=thl_web_rr, - redis_config=thl_redis_config, - include_forwarded=True, - ) - assert fipr.information is not None + # No more prefetch info here. Moved into UserIPHistory.enrich_ips + pass @pytest.mark.usefixtures("user_iphistory_manager_clear_cache") @@ -302,57 +274,86 @@ class TestUserIpHistoryManager: self, thl_web_rr: PostgresConfig, thl_redis_config: RedisConfig, + geoip_info_manager: GeoIpInfoManager, user_iphistory_manager: UserIpHistoryManager, ): instance = UserIpHistoryManager( - pg_config=thl_web_rr, redis_config=thl_redis_config + pg_config=thl_web_rr, + redis_config=thl_redis_config, + geoip_info_manager=geoip_info_manager, ) assert isinstance(instance, UserIpHistoryManager) assert isinstance(user_iphistory_manager, UserIpHistoryManager) - def test_latest_record( + def test_latest_record_and_enrich( self, user_iphistory_manager: UserIpHistoryManager, user: User, ip_record_factory: Callable[..., IPRecord], - ip_information_factory: Callable[..., IPInformation], - ip_geoname: IPGeoname, + geoip_information_factory: Callable[..., GeoIPInformation], + geoip_info_manager: GeoIpInfoManager, ): ip = fake.ipv4_public() - ip_information_factory(ip=ip, geoname_id=ip_geoname.geoname_id, is_anonymous=True) - ipr1: IPRecord = ip_record_factory(user_id=user.user_id, ip=ip) + information = geoip_information_factory( + ip=ip, is_anonymous=True, country_iso="de" + ) + lookup_results = {ip: information} + geoip_info_manager.get_multi = Mock( + side_effect=lambda ip_addresses: { + address: lookup_results[address] for address in ip_addresses + } + ) - ipr = user_iphistory_manager.get_user_latest_ip_record(user=user) - assert isinstance(ipr, IPRecord) + ipr1 = ip_record_factory(user=user, ip=ip) + ipr = user_iphistory_manager.get_user_latest_ip_record( + user=user, + ) assert ipr.ip == ipr1.ip assert ipr.is_anonymous assert isinstance(ipr.information, GeoIPInformation) - assert ipr.information.lookup_prefix == "/32" - ip = fake.ipv6() - ip_information_factory(ip=ip, geoname_id=ip_geoname.geoname_id) - ipr2: IPRecord = ip_record_factory(user_id=user.user_id, ip=ip) + assert ( + user_iphistory_manager.get_user_latest_country( + user=user, + ) + == "de" + ) + + ip2 = fake.ipv6() + ipr2: IPRecord = ip_record_factory(user=user, ip=ip2) + lookup_results[ipr2.ip] = geoip_information_factory( + ip=ipr2.ip, + country_iso="us", + is_anonymous=False, + ) - ipr = user_iphistory_manager.get_user_latest_ip_record(user=user) - assert isinstance(ipr, IPRecord) + ipr = user_iphistory_manager.get_user_latest_ip_record( + user=user, + ) assert ipr.ip == ipr2.ip assert isinstance(ipr.information, GeoIPInformation) - assert ipr.information.lookup_prefix == "/64" assert ipr.information is not None assert not ipr.is_anonymous - country_iso = user_iphistory_manager.get_user_latest_country(user=user) - assert country_iso == ip_geoname.country_iso + assert ( + user_iphistory_manager.get_user_latest_country( + user=user, + ) + == "us" + ) - iph = user_iphistory_manager.get_user_ip_history(user_id=user.user_id) + iph = user_iphistory_manager.get_user_ip_history(user=user) assert isinstance(iph, UserIPHistory) assert isinstance(iph.ips, list) assert iph.ips[0].information is not None assert iph.ips[1].information is not None - assert iph.ips[0].country_iso == country_iso - assert iph.ips[0].is_anonymous - assert iph.ips[0].ip == ipr1.ip - assert iph.ips[1].ip == ipr2.ip + assert iph.ips[0].country_iso == "us" + assert iph.ips[1].country_iso == "de" + assert not iph.ips[0].is_anonymous + assert iph.ips[1].is_anonymous + # ordered by created DESCENDING!!!!!!!!!!!!!!1 + assert iph.ips[0].ip == ipr2.ip + assert iph.ips[1].ip == ipr1.ip def test_virgin( self, @@ -360,65 +361,9 @@ class TestUserIpHistoryManager: user_iphistory_manager: UserIpHistoryManager, ip_record_factory: Callable[..., IPRecord], ): - iph = user_iphistory_manager.get_user_ip_history(user_id=user.user_id) + iph = user_iphistory_manager.get_user_ip_history(user=user) assert len(iph.ips) == 0 - ip_record_factory(user_id=user.user_id, ip=fake.ipv4_public()) - iph = user_iphistory_manager.get_user_ip_history(user_id=user.user_id) + ip_record_factory(user=user, ip=fake.ipv4_public()) + iph = user_iphistory_manager.get_user_ip_history(user=user) assert len(iph.ips) == 1 - - def test_out_of_order( - self, - ip_record_factory: Callable[..., IPRecord], - user: User, - user_iphistory_manager: UserIpHistoryManager, - ip_information_factory: Callable[..., IPInformation], - ip_geoname: IPGeoname, - ): - # Create the user-ip association BEFORE the ip even exists in the ipinfo table - ip = fake.ipv4_public() - ip_record_factory(user_id=user.user_id, ip=ip) - iph = user_iphistory_manager.get_user_ip_history(user_id=user.user_id) - assert isinstance(iph, UserIPHistory) - assert isinstance(iph.ips, list) - assert len(iph.ips) == 1 - ipr = iph.ips[0] - assert ipr.information is None - assert not ipr.is_anonymous - - ip_information_factory(ip=ip, geoname_id=ip_geoname.geoname_id, is_anonymous=True) - iph = user_iphistory_manager.get_user_ip_history(user_id=user.user_id) - assert isinstance(iph, UserIPHistory) - assert isinstance(iph.ips, list) - assert len(iph.ips) == 1 - ipr = iph.ips[0] - assert ipr.information is not None - assert ipr.is_anonymous - - def test_out_of_order_ipv6( - self, - ip_record_factory: Callable[..., IPRecord], - user: User, - user_iphistory_manager: UserIpHistoryManager, - ip_information_factory: Callable[..., IPInformation], - ip_geoname: IPGeoname, - ): - # Create the user-ip association BEFORE the ip even exists in the ipinfo table - ip = fake.ipv6() - ip_record_factory(user_id=user.user_id, ip=ip) - iph = user_iphistory_manager.get_user_ip_history(user_id=user.user_id) - assert isinstance(iph, UserIPHistory) - assert isinstance(iph.ips, list) - assert len(iph.ips) == 1 - ipr = iph.ips[0] - assert ipr.information is None - assert not ipr.is_anonymous - - ip_information_factory(ip=ip, geoname_id=ip_geoname.geoname_id, is_anonymous=True) - iph = user_iphistory_manager.get_user_ip_history(user_id=user.user_id) - assert isinstance(iph, UserIPHistory) - assert isinstance(iph.ips, list) - assert len(iph.ips) == 1 - ipr = iph.ips[0] - assert ipr.information is not None - assert ipr.is_anonymous diff --git a/tests/managers/thl/test_wallet/__init__.py b/tests/managers/thl/test_wallet/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/managers/thl/test_wallet/__init__.py diff --git a/tests/managers/thl/test_wallet/test_user_payout.py b/tests/managers/thl/test_wallet/test_user_payout.py new file mode 100644 index 0000000..bbc97a3 --- /dev/null +++ b/tests/managers/thl/test_wallet/test_user_payout.py @@ -0,0 +1,435 @@ +from datetime import datetime +from decimal import Decimal +from random import randint +from unittest.mock import Mock +from uuid import uuid4 + +import pytest +from faker import Faker + +from generalresearch.currency import USDCent +from generalresearch.managers.thl.cashout_method import CashoutMethodManager +from generalresearch.managers.thl.ipinfo import GeoIpInfoManager +from generalresearch.managers.thl.ledger_manager.thl_ledger import ThlLedgerManager +from generalresearch.managers.thl.userhealth import UserIpHistoryManager +from generalresearch.managers.thl.wallet.tango import TangoManager +from generalresearch.managers.thl.wallet.user_payout import ( + UserPayoutEventManager, + make_request_paypal, +) +from generalresearch.models.thl.definitions import PayoutStatus +from generalresearch.models.thl.payout import UserPayoutEvent +from generalresearch.models.thl.user import User +from generalresearch.models.thl.wallet.cashout_method import ( + CashMailCashoutMethodData, + CashMailCashoutMethodRequestData, + CashoutRequestInfo, + PaypalCashoutMethodData, + PaypalCashoutMethodRequestData, + USDeliveryAddress, +) +from generalresearch.models.thl.wallet.definitions import PayoutType + +fake = Faker() + + +class TestUserPayoutEventManager: + def test_get_by_uuid_and_create( + self, + user: User, + user_payout_event_manager: UserPayoutEventManager, + thl_ledger_manager: ThlLedgerManager, + utc_now: datetime, + ): + user_account = thl_ledger_manager.get_account_or_create_user_wallet(user=user) + request_data = PaypalCashoutMethodRequestData.model_validate( + { + "email": fake.email(), + "interface": "api", + } + ) + pe1: UserPayoutEvent = user_payout_event_manager.create( + debit_account_uuid=user_account.uuid, + payout_type=PayoutType.PAYPAL, + cashout_method_uuid=uuid4().hex, + amount=100, + created=utc_now, + request_data=request_data.model_dump(mode="json"), + ) + # these get added by the query + pe1.account_reference_type = "user" + pe1.account_reference_uuid = user.uuid + + pe2 = user_payout_event_manager.get_by_uuid(pe_uuid=pe1.uuid) + + assert pe1 == pe2 + + def test_get_payout_detail_cash_in_mail( + self, + user_with_wallet: User, + user_payout_event_manager: UserPayoutEventManager, + cashout_method_manager: CashoutMethodManager, + thl_ledger_manager: ThlLedgerManager, + ): + user = user_with_wallet + data = CashMailCashoutMethodData( + delivery_address=USDeliveryAddress.model_validate( + { + "name_or_attn": "Josh Ackerman", + "address": "123 Fake St", + "city": "San Francisco", + "state": "CA", + "postal_code": "12345", + } + ) + ) + cm = cashout_method_manager.create_cash_in_mail_cashout_method( + data=data, user=user + ) + user_account = thl_ledger_manager.get_account_or_create_user_wallet(user=user) + + rand_amount = randint(a=99, b=999) + request_data = CashMailCashoutMethodRequestData.model_validate( + cm.data.model_dump() + ) + + pe = user_payout_event_manager.create( + debit_account_uuid=user_account.uuid, + cashout_method_uuid=cm.id, + amount=rand_amount, + ext_ref_id=uuid4().hex, + payout_type=PayoutType.CASH_IN_MAIL, + request_data=request_data.model_dump(mode="json"), + ) + + res = user_payout_event_manager.get_payout_detail(pe_uuid=pe.uuid) + assert isinstance(res, CashoutRequestInfo) + + def test_get_payout_detail_paypal( + self, + user_with_wallet: User, + user_payout_event_manager: UserPayoutEventManager, + cashout_method_manager: CashoutMethodManager, + thl_ledger_manager: ThlLedgerManager, + ): + user = user_with_wallet + data = PaypalCashoutMethodData(email=fake.email()) + cm = cashout_method_manager.create_paypal_cashout_method(data=data, user=user) + user_account = thl_ledger_manager.get_account_or_create_user_wallet(user=user) + + rand_amount = randint(a=99, b=999) + request_data = make_request_paypal(cm) + pe = user_payout_event_manager.create( + debit_account_uuid=user_account.uuid, + cashout_method_uuid=cm.id, + amount=rand_amount, + ext_ref_id=uuid4().hex, + payout_type=PayoutType.PAYPAL, + request_data=request_data.model_dump(mode="json"), + ) + + res = user_payout_event_manager.get_payout_detail(pe_uuid=pe.uuid) + assert isinstance(res, CashoutRequestInfo) + + +class TestUserRequestRedeem: + @staticmethod + def request_paypal_payout( + *, + user: User, + amount: USDCent, + user_payout_event_manager: UserPayoutEventManager, + cashout_method_manager: CashoutMethodManager, + thl_ledger_manager: ThlLedgerManager, + user_iphistory_manager: UserIpHistoryManager, + ip_record_factory, + thl_redis_client, + ip: str = "8.8.8.8", + fund_amount: Decimal = Decimal("10.00"), + ) -> UserPayoutEvent: + email = fake.email() + cashout_method = cashout_method_manager.create_paypal_cashout_method( + data=PaypalCashoutMethodData(email=email), + user=user, + ) + ip_record_factory(user=user, ip=ip) + thl_ledger_manager.create_tx_user_bonus( + user=user, + amount=fund_amount, + ref_uuid=uuid4().hex, + description="Fund PayPal redemption test", + ) + + tango_manager = Mock(spec=TangoManager) + tango_manager.get_exchange_rates.return_value = {} + + return user_payout_event_manager.user_request_redeem( + user=user, + cashout_method_id=cashout_method.id, + amount=amount, + tango_manager=tango_manager, + cashout_method_manager=cashout_method_manager, + ledger_manager=thl_ledger_manager, + user_ip_history_manager=user_iphistory_manager, + redis_client=thl_redis_client, + ) + + def test_user_request_redeem_paypal( + self, + user_with_wallet: User, + user_payout_event_manager: UserPayoutEventManager, + cashout_method_manager: CashoutMethodManager, + thl_ledger_manager: ThlLedgerManager, + user_iphistory_manager: UserIpHistoryManager, + geoip_info_manager: GeoIpInfoManager, + ip_record_factory, + thl_redis_client, + ): + user = user_with_wallet + payout = self.request_paypal_payout( + user=user, + amount=USDCent(600), + user_payout_event_manager=user_payout_event_manager, + cashout_method_manager=cashout_method_manager, + thl_ledger_manager=thl_ledger_manager, + user_iphistory_manager=user_iphistory_manager, + ip_record_factory=ip_record_factory, + thl_redis_client=thl_redis_client, + ) + + assert payout.status == PayoutStatus.PENDING + assert payout.payout_type == PayoutType.PAYPAL + assert payout.amount == 600 + assert payout.request_data["interface"] == "api" + assert thl_ledger_manager.get_user_wallet_balance(user) == 400 + assert user_payout_event_manager.get_by_uuid(payout.uuid) == payout + + detail = user_payout_event_manager.get_payout_detail(payout.uuid) + assert not detail.transaction_info + assert detail.description == "PayPal" + + def test_user_request_redeem_paypal_complete( + self, + user_with_wallet: User, + user_payout_event_manager: UserPayoutEventManager, + cashout_method_manager: CashoutMethodManager, + thl_ledger_manager: ThlLedgerManager, + user_iphistory_manager: UserIpHistoryManager, + geoip_info_manager: GeoIpInfoManager, + ip_record_factory, + thl_redis_client, + create_main_accounts, + ): + create_main_accounts() + user = user_with_wallet + payout = self.request_paypal_payout( + user=user, + amount=USDCent(600), + user_payout_event_manager=user_payout_event_manager, + cashout_method_manager=cashout_method_manager, + thl_ledger_manager=thl_ledger_manager, + user_iphistory_manager=user_iphistory_manager, + ip_record_factory=ip_record_factory, + thl_redis_client=thl_redis_client, + ) + + user_payout_event_manager.update( + payout_event=payout, + status=PayoutStatus.APPROVED, + ext_ref_id="paypal-batch-id", + ) + thl_ledger_manager.create_tx_user_payout_complete( + user=user, + payout_event=payout, + fee_amount=Decimal("0.25"), + ) + user_payout_event_manager.update( + payout_event=payout, + status=PayoutStatus.COMPLETE, + order_data={"transaction_id": "paypal-transaction-id"}, + ) + + detail = user_payout_event_manager.get_payout_detail(pe_uuid=payout.uuid) + assert detail.status == PayoutStatus.COMPLETE + assert detail.transaction_info == {"transaction_id": "paypal-transaction-id"} + + assert thl_ledger_manager.get_user_wallet_balance(user_with_wallet) == 400 + bp_expense = thl_ledger_manager.get_account_or_create_bp_expense_by_uuid( + product_uuid=user.to_user_ref().product_id, expense_name="paypal" + ) + assert thl_ledger_manager.get_account_balance(bp_expense) == -25 + + def test_user_request_redeem_tango_complete( + self, + user_with_wallet: User, + user_payout_event_manager: UserPayoutEventManager, + cashout_method_manager: CashoutMethodManager, + thl_ledger_manager: ThlLedgerManager, + user_iphistory_manager: UserIpHistoryManager, + ip_record_factory, + thl_redis_client, + create_main_accounts, + setup_cashoutmethod_db, + example_tango_cashout_methods, + ): + create_main_accounts() + setup_cashoutmethod_db() + user = user_with_wallet + # This is the italian Amazon.it gift card + cashout_method = example_tango_cashout_methods[1] + # This is an IP that we have hard-coded by the geoip_info_manager as Italy + ip_record_factory(user=user, ip="2.2.2.2") + + thl_ledger_manager.create_tx_user_bonus( + user=user, + amount=Decimal("5.00"), + ref_uuid=uuid4().hex, + description="Fund Tango redemption test", + ) + + tango_client = Mock() + tango_client.get_exchange_rates.return_value = { + "exchangeRates": [ + {"rewardCurrency": "USD", "baseCurrency": "EUR", "baseFx": 1.14792} + ] + } + tango_manager = TangoManager( + tango_client=tango_client, + tango_account_id="test-account", + tango_customer_id="test-customer", + cashout_method_manager=cashout_method_manager, + ) + request = tango_manager.make_request( + amount=USDCent(2_00), + cashout_method=cashout_method, + payout_event_id=uuid4().hex, + ) + assert request.amount == Decimal(2.0 / 1.14792).quantize( + Decimal("0.01") + ) # ~ 1.74 EUR + + payout = user_payout_event_manager.user_request_redeem( + user=user, + cashout_method_id=cashout_method.id, + amount=USDCent(2_00), + tango_manager=tango_manager, + cashout_method_manager=cashout_method_manager, + ledger_manager=thl_ledger_manager, + user_ip_history_manager=user_iphistory_manager, + redis_client=thl_redis_client, + ) + assert payout.request_data["amount"] == '1.74' + assert payout.request_data["externalRefID"] == payout.uuid + + user_payout_event_manager.update( + payout_event=payout, + status=PayoutStatus.APPROVED, + ext_ref_id="tango-order-id", + ) + thl_ledger_manager.create_tx_user_payout_complete( + user=user, + payout_event=payout, + ) + user_payout_event_manager.update( + payout_event=payout, + status=PayoutStatus.COMPLETE, + order_data={ + "reward": { + "credentialList": [ + { + "credentialType": "giftCardCode", + "credentialValue": "TEST-CODE", + } + ], + "redemptionInstructions": "Use this code at checkout.", + } + }, + ) + + detail = user_payout_event_manager.get_payout_detail(pe_uuid=payout.uuid) + assert payout.payout_type == PayoutType.TANGO + assert detail.status == PayoutStatus.COMPLETE + assert detail.transaction_info == { + "credential_list": [ + { + "credentialType": "giftCardCode", + "credentialValue": "TEST-CODE", + } + ], + "redemption_instructions": "Use this code at checkout.", + } + assert thl_ledger_manager.get_user_wallet_balance(user) == 300 + bp_expense = thl_ledger_manager.get_account_or_create_bp_expense_by_uuid( + product_uuid=user.to_user_ref().product_id, + expense_name="tango", + ) + assert thl_ledger_manager.get_account_balance(bp_expense) == -7 + + def test_user_request_redeem_paypal_insufficient_balance( + self, + user_with_wallet: User, + user_payout_event_manager: UserPayoutEventManager, + cashout_method_manager: CashoutMethodManager, + thl_ledger_manager: ThlLedgerManager, + user_iphistory_manager: UserIpHistoryManager, + ip_record_factory, + thl_redis_client, + ): + with pytest.raises(AssertionError, match="more than their redeemable balance"): + self.request_paypal_payout( + user=user_with_wallet, + amount=USDCent(1_100), + user_payout_event_manager=user_payout_event_manager, + cashout_method_manager=cashout_method_manager, + thl_ledger_manager=thl_ledger_manager, + user_iphistory_manager=user_iphistory_manager, + ip_record_factory=ip_record_factory, + thl_redis_client=thl_redis_client, + ) + + def test_user_request_redeem_paypal_blocked_user( + self, + user_with_wallet: User, + user_payout_event_manager: UserPayoutEventManager, + cashout_method_manager: CashoutMethodManager, + thl_ledger_manager: ThlLedgerManager, + user_iphistory_manager: UserIpHistoryManager, + ip_record_factory, + thl_redis_client, + ): + user_with_wallet.blocked = True + + with pytest.raises(AssertionError, match="Blocked user"): + self.request_paypal_payout( + user=user_with_wallet, + amount=USDCent(600), + user_payout_event_manager=user_payout_event_manager, + cashout_method_manager=cashout_method_manager, + thl_ledger_manager=thl_ledger_manager, + user_iphistory_manager=user_iphistory_manager, + ip_record_factory=ip_record_factory, + thl_redis_client=thl_redis_client, + ) + + def test_user_request_redeem_paypal_anonymous_user( + self, + user_with_wallet: User, + user_payout_event_manager: UserPayoutEventManager, + cashout_method_manager: CashoutMethodManager, + thl_ledger_manager: ThlLedgerManager, + user_iphistory_manager: UserIpHistoryManager, + ip_record_factory, + thl_redis_client, + ): + with pytest.raises(AssertionError, match="Anonymous user"): + self.request_paypal_payout( + user=user_with_wallet, + amount=USDCent(600), + user_payout_event_manager=user_payout_event_manager, + cashout_method_manager=cashout_method_manager, + thl_ledger_manager=thl_ledger_manager, + user_iphistory_manager=user_iphistory_manager, + ip_record_factory=ip_record_factory, + thl_redis_client=thl_redis_client, + ip="1.1.1.1", + ) |
