from collections.abc import Callable from datetime import datetime, timezone from typing import Any from uuid import uuid4 import pytest import requests from generalresearch.currency import USDCent from generalresearch.models.thl.definitions import PayoutStatus from generalresearch.models.thl.payout import UserPayoutEvent from generalresearch.models.thl.wallet.cashout_method import ( CashoutRequestInfo, CashoutRequestResponse, ) from generalresearch.models.thl.wallet.definitions import PayoutType from mypy_boto3_mturk.type_defs import ( GetAssignmentResponseTypeDef, GetHITResponseTypeDef, ) from jb.config import settings from jb.managers.amt import ( APPROVAL_MESSAGE, BONUS_MESSAGE, ) @pytest.fixture def approved_assignment_stubs( assignment_response: GetAssignmentResponseTypeDef, assignment_response_factory_approved: Callable[[str], GetAssignmentResponseTypeDef], amt_assignment_id: str, amt_hit_id: str, hit_response_reviewing: GetHITResponseTypeDef, ) -> Callable[..., list[dict[str, Any]]]: # These are the AMT_CLIENT stubs/mocks that need to be set when running # process_assignment_submitted() which will result in an approved # assignment and sent bonus def _inner( feedback: str = APPROVAL_MESSAGE, override_response: str | None = None, override_approve_response: str | None = None, ) -> list[dict[str, Any]]: response = override_response or assignment_response approve_response = ( override_approve_response or assignment_response_factory_approved(feedback) ) return [ { "operation": "get_assignment", "response": response, "expected_params": {"AssignmentId": amt_assignment_id}, }, { "operation": "approve_assignment", "response": {}, "expected_params": { "AssignmentId": amt_assignment_id, "RequesterFeedback": feedback, "OverrideRejection": False, }, }, { "operation": "get_assignment", "response": approve_response, "expected_params": {"AssignmentId": amt_assignment_id}, }, { "operation": "update_hit_review_status", "response": {}, "expected_params": {"HITId": amt_hit_id, "Revert": False}, }, { "operation": "get_hit", "response": hit_response_reviewing, "expected_params": {"HITId": amt_hit_id}, }, ] return _inner @pytest.fixture def approved_assignment_stubs_w_bonus( approved_assignment_stubs: Callable[..., list[dict[str, Any]]], amt_worker_id: str, amt_assignment_id: str, pe_id: str, ) -> list[dict[str, Any]]: now = datetime.now(tz=timezone.utc) stubs = approved_assignment_stubs().copy() stubs.append( { "operation": "send_bonus", "response": {}, "expected_params": { "WorkerId": amt_worker_id, "BonusAmount": "0.07", "AssignmentId": amt_assignment_id, "Reason": BONUS_MESSAGE, "UniqueRequestToken": pe_id, }, } ) stubs.append( { "operation": "list_bonus_payments", "response": { "BonusPayments": [ { "WorkerId": amt_worker_id, "BonusAmount": "0.07", "AssignmentId": amt_assignment_id, "Reason": BONUS_MESSAGE, "GrantTime": now, } ] }, "expected_params": {"AssignmentId": amt_assignment_id}, } ) return stubs @pytest.fixture def rejected_assignment_stubs( assignment_response: GetAssignmentResponseTypeDef, assignment_response_factory_rejected: Callable[[str], GetAssignmentResponseTypeDef], amt_assignment_id: str, amt_hit_id: str, hit_response_reviewing: GetHITResponseTypeDef, ) -> Callable[..., list[dict[str, Any]]]: # These are the AMT_CLIENT stubs/mocks that need to be set when running # process_assignment_submitted() which will result in a rejected # assignment def _inner( reject_reason: str, override_response: str | None = None, override_reject_response: str | None = None, ) -> list[dict[str, Any]]: response = override_response or assignment_response reject_response = ( override_reject_response or assignment_response_factory_rejected(reject_reason) ) return [ { "operation": "get_assignment", "response": response, "expected_params": {"AssignmentId": amt_assignment_id}, }, { "operation": "reject_assignment", "response": {}, "expected_params": { "AssignmentId": amt_assignment_id, "RequesterFeedback": reject_reason, }, }, { "operation": "get_assignment", "response": reject_response, "expected_params": {"AssignmentId": amt_assignment_id}, }, { "operation": "update_hit_review_status", "response": {}, "expected_params": {"HITId": amt_hit_id, "Revert": False}, }, { "operation": "get_hit", "response": hit_response_reviewing, "expected_params": {"HITId": amt_hit_id}, }, ] return _inner @pytest.fixture def mock_thl_responses( monkeypatch: pytest.MonkeyPatch, amt_worker_id: str, tsid: str, pe_id: str ) -> Callable[..., None]: original_get = requests.get original_post = requests.post class MockThlCashoutRequestResponse: def json(self): return CashoutRequestResponse( status="success", cashout=CashoutRequestInfo( id=pe_id, description="amt something", status=PayoutStatus.PENDING, ), ).model_dump(mode="json") def _inner( user_blocked: bool = False, status_finished: bool = True, status_complete: bool = False, wallet_redeemable_amount: int = 10, ): def mock_get(url, *args, **kwargs): # type: ignore profile_url = f"{settings.fsb_host}{settings.product_id}/user/{amt_worker_id}/profile/" status_url = f"{settings.fsb_host}{settings.product_id}/status/{tsid}/" cashout_request_url = f"{settings.fsb_host}{settings.product_id}/cashout/" wallet_url = f"{settings.fsb_host}{settings.product_id}/wallet/" if url == profile_url: class MockThlProfileResponse: def json(self): return { "user_profile": { "user": { "product_id": settings.product_id, "product_user_id": amt_worker_id, "blocked": user_blocked, } } } return MockThlProfileResponse() elif url == wallet_url: class MockThlWalletResponse: def json(self) -> dict[str, Any]: return { "wallet": { "amount": wallet_redeemable_amount, "redeemable_amount": wallet_redeemable_amount, } } return MockThlWalletResponse() elif url == status_url: class MockThlStatusResponse: def json(self) -> dict[str, Any]: return { "tsid": tsid, "product_id": str(settings.product_id), "product_user_id": amt_worker_id, "started": "2020-06-02T00:30:35.036398Z", "finished": ( "2020-06-02T00:31:35.036398Z" if status_finished else None ), "status": 3 if status_complete else 2, "payout": 10 if status_complete else 0, "user_payout": 10 if status_complete else 0, "status_code_1": "BUYER_FAIL", "status_code_2": None, } return MockThlStatusResponse() elif url == cashout_request_url: return MockThlCashoutRequestResponse() else: raise ValueError(f"unhandled call: {url=} {args=} {kwargs=}") return original_get(url, *args, **kwargs) def mock_post(url, *args, **kwargs): # type: ignore cashout_request_url = f"{settings.fsb_host}{settings.product_id}/cashout/" manage_cashout_request_url = f"{settings.fsb_host}{settings.fsb_host_private_route}/thl/manage_cashout/" if url == cashout_request_url: return MockThlCashoutRequestResponse() elif url == manage_cashout_request_url: json = kwargs["json"] print(json) payout_id = json["payout_id"] new_status = json["new_status"] class MockThlManageCashoutResponse: def json(self): return UserPayoutEvent( uuid=payout_id, status=new_status, amount=USDCent(5), debit_account_uuid=uuid4().hex, cashout_method_uuid=uuid4().hex, payout_type=PayoutType.AMT, ).model_dump(mode="json") return MockThlManageCashoutResponse() return original_post(url, *args, **kwargs) monkeypatch.setattr(requests, "get", mock_get) monkeypatch.setattr(requests, "post", mock_post) return _inner