import json from typing import TypedDict from urllib import error, parse, request class PayoutResult(TypedDict): status: str reason_code: str | None reason_description: str | None def fetch_payout_status( api_url: str, auth_token: str, program_id: str, client_reference_id: str, timeout_seconds: int, ) -> PayoutResult: safe_program_id = parse.quote(str(program_id), safe="") safe_client_reference_id = parse.quote(str(client_reference_id), safe="") url = ( f"{api_url.rstrip('/')}" f"/v4/programs/{safe_program_id}/payouts/{safe_client_reference_id}/status" ) req = request.Request( url=url, method="GET", headers={ "Authorization": f"Bearer {auth_token}", "Accept": "application/json", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", }, ) try: with request.urlopen(req, timeout=timeout_seconds) as response: payload = json.loads(response.read().decode("utf-8")) except error.HTTPError as exc: body = exc.read().decode("utf-8", errors="replace") raise error.HTTPError( exc.url, exc.code, f"{exc.reason} — {body}", exc.headers, None ) from exc result = payload.get("result") if isinstance(payload, dict) else None if not isinstance(result, dict) or "status" not in result: raise ValueError("Payoneer response does not contain result.status") # Payoneer uses reason_code/reason_description when the bank rejects the # payment, and cancel_reason_code/cancel_reason_description when the # payment is cancelled. Only one pair is ever present at a time. reason_code = result.get("reason_code", result.get("cancel_reason_code")) reason_description = result.get("reason_description", result.get("cancel_reason_description")) return PayoutResult( status=str(result["status"]), reason_code=str(reason_code) if reason_code is not None else None, reason_description=reason_description, )