from datetime import timedelta from typing import Annotated from urllib.parse import urlencode from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import HTMLResponse from generalresearch.models.thl.definitions import PayoutStatus from generalresearch.models.thl.wallet.cashout_method import CashoutRequestInfo from generalresearch.redis_helper import RedisConfig from jb.api.auth import get_authenticated_user from jb.api.cashout_token import consume_cashout_token, create_cashout_token from jb.config import settings from jb.decorators import get_redis_config from jb.dependencies import get_gr_api_manager from jb.managers.email_manager import ( send_cashout_confirmation_email, send_cashout_status_email, ) from jb.managers.gr_api import GRApiManager from jb.managers.thl import ( get_cashout_detail, get_cashout_method, user_cashout_request, ) from jb.models.auth import User from jb.models.wallet import ( CashoutConfirmation, CashoutPostback, CashoutRequest, PendingCashout, ) from jb.settings import render_base_html wallet_router = APIRouter(prefix="/wallet", tags=["Wallet"]) @wallet_router.post("/cashout/request/") def request_cashout( body: CashoutRequest, user: Annotated[User, Depends(get_authenticated_user)], ) -> dict[str, str]: """Email the authenticated user a link confirming the requested amount.""" token = create_cashout_token( PendingCashout( product_user_id=user.product_user_id, amount=body.amount, cashout_method_id=body.cashout_method_id, ) ) query = urlencode({"token": token}) confirmation_link = f"{settings.base_url}wallet/cashout/confirm/?{query}" cm = get_cashout_method(cashout_method_id=body.cashout_method_id) if settings.debug: return {"confirmation_link": confirmation_link, "cashout_method": cm.id} send_cashout_confirmation_email( email=str(user.email), token=token, cashout_method=cm, amount=body.amount ) return {"detail": "Confirmation sent. Check your inbox to finish the cashout."} @wallet_router.get( "/cashout/confirm/", response_class=HTMLResponse, include_in_schema=False ) def cashout_confirmation_page() -> HTMLResponse: """Serve the SPA without consuming the token; email prefetches are harmless.""" return HTMLResponse( render_base_html(), headers={ "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "X-Robots-Tag": "noindex, nofollow", }, ) @wallet_router.post("/cashout/confirm/", response_model=CashoutRequestInfo) def confirm_cashout( body: CashoutConfirmation, user: Annotated[User, Depends(get_authenticated_user)], ) -> CashoutRequestInfo: cashout = consume_cashout_token(body.token) return user_cashout_request( product_user_id=cashout.product_user_id, amount=cashout.amount, cashout_method_id=cashout.cashout_method_id, ) @wallet_router.post("/cashout/postback/", status_code=status.HTTP_204_NO_CONTENT) def cashout_postback( body: CashoutPostback, gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], redis_config: Annotated[RedisConfig, Depends(get_redis_config)], ) -> None: # Treat the posted ID only as a lookup key; THL supplies the trusted details. try: cashout = get_cashout_detail(body.cashout_id) except Exception as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Cashout not found: {e}", ) if cashout.product_id != settings.product_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cashout not found", ) if cashout.status != PayoutStatus.COMPLETE: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Cashout is not complete", ) # In case THL retries, send at most one email for each dedupe_key = f"wallet:cashout-email-sent:{cashout.id}:{cashout.status.value}" redis_client = redis_config.create_redis_client() claimed = redis_client.set(dedupe_key, "sending", nx=True, ex=timedelta(minutes=5)) if not claimed: return try: user = gr_api.get_user(product_user_id=cashout.product_user_id) send_cashout_status_email(email=str(user.email), cashout=cashout) redis_client.set(dedupe_key, "sent", ex=timedelta(minutes=30)) except Exception: # Allow a later retry when user lookup or email delivery fails. redis_client.delete(dedupe_key) raise