1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
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 CashoutRequestDetail
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=CashoutRequestDetail)
def confirm_cashout(
body: CashoutConfirmation,
user: Annotated[User, Depends(get_authenticated_user)],
) -> CashoutRequestDetail:
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
|