from datetime import timezone, datetime from typing import Dict, Callable, Any, Optional from uuid import uuid4 import pytest import requests from generalresearchutils.models.thl.payout import UserPayoutEvent from generalresearchutils.models.thl.wallet import PayoutType from generalresearchutils.models.thl.wallet.cashout_method import ( CashoutRequestResponse, CashoutRequestInfo, ) from mypy_boto3_mturk.type_defs import ( GetHITResponseTypeDef, GetAssignmentResponseTypeDef, ) from jb.config import settings from jb.managers.amt import ( APPROVAL_MESSAGE, BONUS_MESSAGE, ) from generalresearchutils.currency import USDCent from generalresearchutils.models.thl.definitions import PayoutStatus @pytest.fixture def approved_assignment_stubs( get_assignment_response: GetAssignmentResponseTypeDef, get_assignment_response_approved: Callable[[str], GetAssignmentResponseTypeDef], amt_assignment_id: str, amt_hit_id: str, get_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: Optional[str] = None, override_approve_response: Optional[str] = None, ) -> list[Dict[str, Any]]: response = override_response or get_assignment_response approve_response = ( override_approve_response or get_assignment_response_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": get_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: Optional[str] = None, override_reject_response: Optional[str] = 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": {"blocked": user_blocked}}} return MockThlProfileResponse() elif url == wallet_url: class MockThlWalletResponse: def json(self) -> Dict[str, Any]: return { "wallet": {"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