diff options
| author | Max Nanis | 2026-09-13 19:03:55 +0000 |
|---|---|---|
| committer | Max Nanis | 2026-09-13 19:03:55 +0000 |
| commit | 8fbe8d439b418796932aaa88297e006c714e4d13 (patch) | |
| tree | 6c800476edc1e771fc570b559d483df8d59d4324 | |
| parent | 12f6fee851b68e86af658dfa17e4a0daed457dd1 (diff) | |
| parent | 2c94f248d2438071a918fa9a30bf114ef9aa29b4 (diff) | |
| download | amt-jb-8fbe8d439b418796932aaa88297e006c714e4d13.tar.gz amt-jb-8fbe8d439b418796932aaa88297e006c714e4d13.zip | |
Merges pull request #3
Off of Amazon!!!
152 files changed, 7866 insertions, 6253 deletions
@@ -150,10 +150,12 @@ static-src/node_modules # Settings .env* + # Carer (remove everything + selectively allow) /carer/carer/settings/* !/carer/carer/settings/base.py !/carer/carer/settings/unittest.py +/carer/app/test_settings.py # dependencies /jb-ui/node_modules diff --git a/Jenkinsfile b/Jenkinsfile index eb53bfd..5f63edc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,7 +1,3 @@ -/* This is intended to test the dev & master branches of the gr-api project, - where the tests are entirely self contained -*/ - pipeline { agent any @@ -10,95 +6,85 @@ pipeline { pollSCM('H */3 * * *') } + options { + skipDefaultCheckout() + } + environment { DATA_SRC = "${env.WORKSPACE}/mnt/" - - AMT_JB_CARER_VENV = "${env.WORKSPACE}/amt-jb-carer-venv" - AMT_JB_VENV = "${env.WORKSPACE}/amt-jb-venv" + VENV = "${env.WORKSPACE}/amt-jb-venv" } stages { - stage('Setup DB') { - steps { - script { - env.DB_NAME = 'unittest-amt-jb-' + UUID.randomUUID().toString().replace('-', '').take(12) - env.AMT_JB_DB = "postgres://${env.DB_USER}:${env.DB_PASSWORD}@${env.DB_POSTGRESQL_HOST}/${env.DB_NAME}" - echo "Using database: ${env.DB_NAME}" - } - - sh """ - PGPASSWORD=${env.DB_PASSWORD} psql -h ${env.DB_POSTGRESQL_HOST} -U ${env.DB_USER} -d postgres <<EOF - CREATE DATABASE "${env.DB_NAME}" WITH TEMPLATE = template0 ENCODING = 'UTF8'; - EOF - """ - } - } - - stage('env') { - steps { - withCredentials([file( - credentialsId: 'amt-jb-app-env', - variable: 'ENV_FILE_PATH')]) { - sh "cp \$ENV_FILE_PATH ${WORKSPACE}/.env.test" - } - } - } - - stage('setup:amt-jb-carer') { + stage('Checkout') { steps { checkout scmGit( - branches: [[name: 'master']], + branches: [[name: "*/${env.BRANCH_NAME}"]], extensions: [ cloneOption(shallow: true) ], userRemoteConfigs: [ - [credentialsId: 'abdeb570-b708-44f3-b857-8a6b06ed9822', + [credentialsId: 'abdeb570-b708-44f3-b857-8a6b06ed9822', url: 'ssh://code.g-r-l.com:6611/panels/amt-jb'] ], ) - - dir('carer/') { - sh 'python3.13 -m venv $AMT_JB_CARER_VENV' - sh '$AMT_JB_CARER_VENV/bin/pip install -U setuptools wheel pip' - sh '$AMT_JB_CARER_VENV/bin/pip install -r requirements.txt' - sh '$AMT_JB_CARER_VENV/bin/python3.13 manage.py migrate --settings=carer.settings.unittest' - } + stash name: 'source', useDefaultExcludes: false } } - /* Okay, finally we can setup the virtual environment for the actual - project itself. gr-api FastAPI doesn't manage any of it's own - database so it doesn't need to do any migrations or anything. - */ - stage('setup:amt-jb') { - steps { - sh 'python3.13 -m venv $AMT_JB_VENV' - sh '$AMT_JB_VENV/bin/pip install -U setuptools wheel pip' - withCredentials([sshUserPrivateKey( - credentialsId: 'abdeb570-b708-44f3-b857-8a6b06ed9822', - keyFileVariable: 'SSH_PRIVATE_KEY')]) { - sh """ - eval \$(ssh-agent) && ssh-add ${SSH_PRIVATE_KEY} && \ - ${AMT_JB_VENV}/bin/pip install -r requirements.txt - """ + stage('Python Versions') { + matrix { + axes { + axis { + name 'VER' + values 'python3.14' } - } - } + } - stage('tests') { - steps { - sh '$AMT_JB_VENV/bin/pytest -v tests' + stages { + stage('Setup') { + steps { + dir("amt-jb-${VER}") { + deleteDir() + unstash 'source' + + withCredentials([file( + credentialsId: '5cfa9a3a-3c26-499d-832f-0916d524fe05', + variable: 'ENV_TEST_FILE')]) { + sh 'cp $ENV_TEST_FILE .env.test' + } + + sh "/usr/local/bin/${VER} -m venv ${VENV}-${VER}" + withCredentials([sshUserPrivateKey( + credentialsId: 'abdeb570-b708-44f3-b857-8a6b06ed9822', + keyFileVariable: 'SSH_PRIVATE_KEY')]) { + sh """ + eval \$(ssh-agent) && ssh-add ${SSH_PRIVATE_KEY} && \ + ${VENV}-${VER}/bin/pip install -r requirements.txt + ${VENV}-${VER}/bin/pip install django + """ + } + } + } + } + + stage('tests') { + steps { + dir("amt-jb-${VER}") { + sh "${VENV}-${VER}/bin/pytest tests/test_postgres.py -vs" + sh "${VENV}-${VER}/bin/pytest tests/models -vs" + sh "${VENV}-${VER}/bin/pytest tests/managers -vs" + sh "${VENV}-${VER}/bin/pytest tests/http -vs" + } + } + } + } } } } + post { always { echo 'One way or another, I have finished' deleteDir() /* clean up our workspace */ - - sh """ - PGPASSWORD=${env.DB_PASSWORD} psql -h ${env.DB_POSTGRESQL_HOST} -U ${env.DB_USER} -d postgres <<EOF - DROP DATABASE "${env.DB_NAME}"; - EOF - """ } } } diff --git a/amt-jb.conf b/amt-jb.conf index c6edf5f..8bd87c6 100644 --- a/amt-jb.conf +++ b/amt-jb.conf @@ -9,4 +9,6 @@ autorestart=true numprocs=2 numprocs_start=1 process_name=uvicorn-%(process_num)d -environment=LD_LIBRARY_PATH="/usr/local/lib:"
\ No newline at end of file +environment=LD_LIBRARY_PATH="/usr/local/lib:" +stopasgroup=true +killasgroup=true
\ No newline at end of file diff --git a/__init__.py b/carer/app/__init__.py index e69de29..e69de29 100644 --- a/__init__.py +++ b/carer/app/__init__.py diff --git a/carer/app/manage.py b/carer/app/manage.py new file mode 100644 index 0000000..9ae482e --- /dev/null +++ b/carer/app/manage.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "carer.app.settings") + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/carer/app/settings.py b/carer/app/settings.py new file mode 100644 index 0000000..55dd93b --- /dev/null +++ b/carer/app/settings.py @@ -0,0 +1,24 @@ +import os + +INSTALLED_APPS = [ + "django.contrib.postgres", + "django.contrib.contenttypes", + "carer.mtwerk", +] + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("DB_NAME", "thl-test"), + "USER": os.environ.get("DB_USER", "postgres"), + "PASSWORD": os.environ.get("DB_PASSWORD", "password"), + "HOST": os.environ.get("DB_HOST", "127.0.0.1"), + "PORT": "5432", + } +} +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_L10N = True +USE_TZ = True diff --git a/carer/apps.py b/carer/apps.py new file mode 100644 index 0000000..c96687b --- /dev/null +++ b/carer/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class MTwerkSchemaConfig(AppConfig): + name = "carer.mtwerk" + label = "mtwerk" + + def ready(self): + from .mtwerk import models # pycharm: keep diff --git a/carer/carer/settings/base.py b/carer/carer/settings/base.py deleted file mode 100644 index c83e0fe..0000000 --- a/carer/carer/settings/base.py +++ /dev/null @@ -1,18 +0,0 @@ -from pathlib import Path -import os - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -SECRET_KEY = os.environ.get("CARER_SECRET_KEY") - -INSTALLED_APPS = [ - "carer.mtwerk", -] - -LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" -USE_I18N = True -USE_TZ = True - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/carer/carer/settings/unittest.py b/carer/carer/settings/unittest.py deleted file mode 100644 index a147794..0000000 --- a/carer/carer/settings/unittest.py +++ /dev/null @@ -1,30 +0,0 @@ -import os - -# noinspection PyUnresolvedReferences -from .base import * - -DEBUG = True -ALLOWED_HOSTS = ["*"] - -DEFAULT_HTTP_PROTOCOL = "https" -CSRF_COOKIE_HTTPONLY = True # Get from page, never cookie - -DB_NAME = os.environ.get("DB_NAME") -DB_USER = os.environ.get("DB_USER") -DB_PASSWORD = os.environ.get("DB_PASSWORD") -DB_POSTGRESQL_HOST = os.environ.get("DB_POSTGRESQL_HOST") - -DATABASES = dict( - default={ - "ENGINE": "django.db.backends.postgresql", - "NAME": DB_NAME, - "USER": DB_USER, - "PASSWORD": DB_PASSWORD, - "HOST": DB_POSTGRESQL_HOST, - "PORT": "5432", - "ENCODING": "utf8mb4", - "COLLATION": "utf8mb4_unicode_520_ci", - } -) - -print(f"Using test database: {DB_NAME} on {DATABASES['default']['HOST']}") diff --git a/carer/manage.py b/carer/manage.py deleted file mode 100644 index 091b39b..0000000 --- a/carer/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "carer.settings") - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - main() diff --git a/carer/carer/__init__.py b/carer/mtwerk/__init__.py index e69de29..e69de29 100644 --- a/carer/carer/__init__.py +++ b/carer/mtwerk/__init__.py diff --git a/carer/carer/mtwerk/migrations/0001_initial.py b/carer/mtwerk/migrations/0001_initial.py index d5c856a..d5c856a 100644 --- a/carer/carer/mtwerk/migrations/0001_initial.py +++ b/carer/mtwerk/migrations/0001_initial.py diff --git a/carer/carer/mtwerk/__init__.py b/carer/mtwerk/migrations/__init__.py index e69de29..e69de29 100644 --- a/carer/carer/mtwerk/__init__.py +++ b/carer/mtwerk/migrations/__init__.py diff --git a/carer/carer/mtwerk/models.py b/carer/mtwerk/models.py index 5cb026d..5cb026d 100644 --- a/carer/carer/mtwerk/models.py +++ b/carer/mtwerk/models.py diff --git a/jb-ui/.vscode/settings.json b/jb-ui/.vscode/settings.json index aef94ef..9b8250c 100644 --- a/jb-ui/.vscode/settings.json +++ b/jb-ui/.vscode/settings.json @@ -1,3 +1,15 @@ { - "editor.rulers": [80, 120] + "editor.rulers": [ + 80, + 120 + ], + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } }
\ No newline at end of file diff --git a/jb-ui/Jenkinsfile b/jb-ui/Jenkinsfile index 27a0ab9..97b8771 100644 --- a/jb-ui/Jenkinsfile +++ b/jb-ui/Jenkinsfile @@ -82,7 +82,7 @@ pipeline { expression { env.BRANCH_NAME == 'master' } } steps { - sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ thl-cdn:/root/amt-jb-cdn/" + sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ cdn-jb:/var/www/amt-jb-cdn/" } } @@ -99,9 +99,9 @@ pipeline { dir(env.BUILD_DIR) { sh "cp ${env.JS_FILENAME}.js ${versioned_js}.js" - sh "cp ${env.CSS_FILENAME}.js ${versioned_css}.js" + sh "cp ${env.CSS_FILENAME}.css ${versioned_css}.css" - sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ thl-cdn:/root/amt-jb-cdn/" + sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ cdn-jb:/var/www/amt-jb-cdn/" } } } @@ -124,9 +124,9 @@ pipeline { dir(env.BUILD_DIR) { sh "cp ${env.JS_FILENAME}.js ${versioned}.js" - sh "cp ${env.CSS_FILENAME}.js ${versioned}.js" + sh "cp ${env.CSS_FILENAME}.css ${versioned}.css" - sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ thl-cdn:/root/amt-jb-cdn/" + sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ cdn-jb:/var/www/amt-jb-cdn/" } } } diff --git a/jb-ui/src/JBApp.tsx b/jb-ui/src/JBApp.tsx index 76e3f29..c55cd73 100644 --- a/jb-ui/src/JBApp.tsx +++ b/jb-ui/src/JBApp.tsx @@ -1,29 +1,52 @@ import { - EventMessage, - EventType, - PingMessage, - PongMessage, - PongMessageKindEnum, - StatsMessage, - StatusApi, - SubscribeMessage, SubscribeMessageKindEnum, - TaskStatusResponse, - UserWalletBalance, - UserWalletBalanceResponse, - WalletApi + EventMessage, + EventType, + PingMessage, + PongMessage, + PongMessageKindEnum, + StatsMessage, + StatusApi, + SubscribeMessage, + SubscribeMessageKindEnum, + TaskStatusResponse, + UserWalletBalance, + UserWalletBalanceResponse, + UserLedgerWallets, + UserLedgerWalletAccountTypeEnum, + WalletApi, + CashoutMethodsResponse, } from "@/api_fsb"; import Footer from "@/components/Footer"; import { useAppDispatch } from "@/hooks"; import { bpid, routeBasename, tagManager } from "@/lib/utils"; -import { setAssignmentID, setProductUserID, setTurkSubmitTo } from "@/models/appSlice"; +import { useGetMeQuery } from "@/models/authAPI"; +import { + setAssignmentID, + setProductUserID, + setUserDisplayedWalletBalance, + setUserWallet, + setUserAttemptCreditWallet, +} from "@/models/appSlice"; + +import { addCashoutMethods } from "@/models/cashoutMethodsSlice"; + import { addEvent } from "@/models/grlEventsSlice"; import { addStatsData } from "@/models/grlStatsSlice"; -import Home from "@/pages/Home"; -import Preview from "@/pages/Preview"; +import MagicLink from "@/pages/MagicLink"; import Result from "@/pages/Result"; +import clsx from "clsx"; + import Work from "@/pages/Work"; -import { BrowserRouter, Outlet, Route, Routes, useSearchParams } from "react-router-dom"; -import useWebSocket from 'react-use-websocket'; +import Transfer from "@/pages/Transfer"; +import { + BrowserRouter, + Outlet, + useMatch, + Route, + Routes, + useSearchParams, +} from "react-router-dom"; +import useWebSocket from "react-use-websocket"; import NewsTicker from "./components/EventMarquee"; import { useEffect } from "react"; @@ -32,178 +55,234 @@ import Wallet from "@/components/Wallet"; import "@/index.css"; import { setTaskStatus, setUserWalletBalance } from "@/models/appSlice"; import { Profiling } from "./components/Profiling"; +import Redemption from "./components/Redemption"; -type Message = PingMessage | PongMessage | SubscribeMessage | EventMessage | StatsMessage; +type Message = + | PingMessage + | PongMessage + | SubscribeMessage + | EventMessage + | StatsMessage; function isStatsMessage(msg: Message): msg is StatsMessage { - return msg.kind === 'stats'; + return msg.kind === "stats"; } function isEventMessage(msg: Message): msg is EventMessage { - return msg.kind === 'event'; + return msg.kind === "event"; } function isPingMessage(msg: Message): msg is PingMessage { - return msg.kind === 'ping'; + return msg.kind === "ping"; } function QueryParamProcessor() { - const dispatch = useAppDispatch() - const [searchParams] = useSearchParams(); + const dispatch = useAppDispatch(); + const [searchParams] = useSearchParams(); + const { data: user } = useGetMeQuery(); - useEffect(() => { - const worker_id = searchParams.get('workerId'); - const assignment_id = searchParams.get('assignmentId'); - const tsid = searchParams.get('tsid'); - const turkSubmitTo = searchParams.get('turkSubmitTo'); + useEffect(() => { + if (user?.product_user_id) { + dispatch(setProductUserID(user.product_user_id)); - if (turkSubmitTo) { - dispatch(setTurkSubmitTo(turkSubmitTo)) - } + new WalletApi() + .getCashoutMethodsProductIdCashoutMethodsGet(bpid, user.product_user_id) + .then((res) => { + const response = res.data as CashoutMethodsResponse; + dispatch(addCashoutMethods(response.cashout_methods)); + }); - if (worker_id) { - dispatch(setProductUserID(worker_id)) - - new WalletApi().getUserWalletBalanceProductIdWalletGet( - bpid, // productId - worker_id, // bpuid - ).then(res => { - const response = res.data as UserWalletBalanceResponse; - const balance = response.wallet as UserWalletBalance; - dispatch(setUserWalletBalance(balance)); - }); - } + new WalletApi() + .getUserWalletBalanceProductIdWalletGet( + bpid, // productId + user.product_user_id, + ) + .then((res) => { + const response = res.data as UserWalletBalanceResponse; + const balance = response.wallet as UserWalletBalance; + dispatch(setUserWalletBalance(balance)); + }); - if (assignment_id) { - dispatch(setAssignmentID(assignment_id)) - } + new WalletApi() + .getUserWalletsProductIdWalletsGet(bpid, user.product_user_id) + .then((res) => { + const response = res.data as UserLedgerWallets; - if (tsid) { - new StatusApi().getTaskStatusProductIdStatusTsidGet( - bpid, // productId - tsid).then(res => { - const response: TaskStatusResponse = res.data; - dispatch(setTaskStatus(response)) - }).catch((error) => { - console.error("Error fetching task status:", error); - }); - } + if (response.displayed_balances) { + setUserDisplayedWalletBalance(response.displayed_balances[0]); + } - }, [searchParams]); + const userWallet = response.wallets?.find( + (w) => + w.account_type === UserLedgerWalletAccountTypeEnum.UserWallet, + ); + if (userWallet) { + setUserWallet(userWallet); + } - return <Outlet />; + const userAttemptCreditWallet = response.wallets?.find( + (w) => + w.account_type === + UserLedgerWalletAccountTypeEnum.UserAttemptCredit, + ); + if (userAttemptCreditWallet) { + setUserAttemptCreditWallet(userAttemptCreditWallet); + } + }); + } + }, [user]); + + useEffect(() => { + const assignment_id = searchParams.get("assignmentId"); + const tsid = searchParams.get("tsid"); + const worker_id = searchParams.get("workerId"); + + if (worker_id) { + dispatch(setProductUserID(worker_id)); + } + + if (assignment_id) { + dispatch(setAssignmentID(assignment_id)); + } + + if (tsid) { + new StatusApi() + .getTaskStatusProductIdStatusTsidGet( + bpid, // productId + tsid, + ) + .then((res) => { + const response: TaskStatusResponse = res.data; + dispatch(setTaskStatus(response)); + }) + .catch((error) => { + console.error("Error fetching task status:", error); + }); + } + }, [searchParams]); + + return <Outlet />; } const Layout = () => { - return ( - <div className="flex flex-col min-h-screen"> - <header className="bg-amber-400 text-amber-950 py-2 text-center"> - <a href="https://www.reddit.com/r/Marketresearch/comments/1s2tl0o/open_letter_to_cint_supplier_quality_issues/" target="_blank"> - <h4 className="text-[12px]"> - Letter to Cint: supplier quality issues, and malware-linked panels. - </h4> - </a> - </header> - - <main className="flex flex-1 bg-zinc-200"> - <div className="flex flex-col items-center justify-center flex-1"> - <div className=" - w-full max-w-full - p-2 bg-white shadow-lg - text-center space-y-4 - md:max-w-5/8 md:rounded-lg - rounded-none - "> - <Wallet /> - <Profiling /> - <Outlet /> - </div> - </div> - </main > - - <footer className="bg-slate-700 text-zinc-200 p-2 text-center font-mono"> - <NewsTicker /> - <Footer /> - </footer> - - </div > - ); + const isWork = useMatch("/work/*"); + const isPreview = useMatch("/preview/*"); + const isSpecialSubview = Boolean(isWork || isPreview); + + return ( + <div className="flex flex-col min-h-screen"> + <header className="bg-amber-400 text-amber-950 py-2 text-center"> + <a + href="https://www.reddit.com/r/Marketresearch/comments/1s2tl0o/open_letter_to_cint_supplier_quality_issues/" + target="_blank" + > + <h4 className="text-[12px]"> + Letter to Cint: supplier quality issues, and malware-linked panels. + </h4> + </a> + </header> + + <main className="flex flex-1 bg-zinc-200"> + <div className="flex flex-col items-center justify-center flex-1"> + <div + className={clsx( + "w-full max-w-full p-2 bg-white shadow-lg text-center space-y-4 md:max-w-5/8 md:rounded-lg rounded-none", + isSpecialSubview && "border-6 border-amber-400", + )} + > + <Wallet /> + <Profiling /> + <Redemption /> + <Outlet /> + </div> + </div> + </main> + + <footer className="bg-slate-700 text-zinc-200 p-2 text-center font-mono"> + <NewsTicker /> + <Footer /> + </footer> + </div> + ); }; function JBApp() { - const dispatch = useAppDispatch() - - const { sendJsonMessage, lastMessage } = useWebSocket( - "wss://fsb.generalresearch.com/ws/events/", { - onOpen: () => { - const subscribe: SubscribeMessage = { - "kind": SubscribeMessageKindEnum.Subscribe, - "product_id": bpid - } - sendJsonMessage(subscribe) + const dispatch = useAppDispatch(); + + const { sendJsonMessage, lastMessage } = useWebSocket( + "wss://fsb.generalresearch.com/ws/events/", + { + onOpen: () => { + const subscribe: SubscribeMessage = { + kind: SubscribeMessageKindEnum.Subscribe, + product_id: bpid, + }; + sendJsonMessage(subscribe); + }, + }, + ); + + useEffect(() => { + if (lastMessage !== null) { + let msg: Message = JSON.parse(lastMessage.data); + + if (isStatsMessage(msg)) { + dispatch(addStatsData(msg.data)); + } else if (isEventMessage(msg)) { + // For simplicity, we just log the event messages here. + // You can extend this to update the UI or trigger notifications as needed. + + switch (msg.data.event_type) { + case EventType.TaskEnter: + case EventType.TaskFinish: + case EventType.UserCreated: + dispatch(addEvent(msg.data)); + break; + + default: + // do nothing for now + break; } + } else if (isPingMessage(msg)) { + const pong: PongMessage = { + kind: PongMessageKindEnum.Pong, + }; + sendJsonMessage(pong); + } } - ); - - useEffect(() => { - if (lastMessage !== null) { - let msg: Message = JSON.parse(lastMessage.data); - - if (isStatsMessage(msg)) { - dispatch(addStatsData(msg.data)) - - } else if (isEventMessage(msg)) { - // For simplicity, we just log the event messages here. - // You can extend this to update the UI or trigger notifications as needed. - - switch (msg.data.event_type) { - case EventType.TaskEnter: - case EventType.TaskFinish: - case EventType.UserCreated: - dispatch(addEvent(msg.data)); - break - - default: - // do nothing for now - break; - } - - } else if (isPingMessage(msg)) { - const pong: PongMessage = { - "kind": PongMessageKindEnum.Pong, - } - sendJsonMessage(pong) - } - } - }, [lastMessage]); - - useEffect(() => { - if (!tagManager) return; - - var _mtm = window._mtm = window._mtm || []; - _mtm.push({ 'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start' }); - var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0]; - g.async = true; - g.src = tagManager; - if (s.parentNode) { - s.parentNode.insertBefore(g, s); - } - }, [tagManager]) - - return ( - <BrowserRouter basename={routeBasename}> - <Routes> - <Route element={<QueryParamProcessor />}> - <Route element={<Layout />}> - <Route path="/" element={<Home />} /> - <Route path="/preview/" element={<Preview />} /> - <Route path="/work/" element={<Work />} /> - <Route path="/result/" element={<Result />} /> - </Route> - </Route> - </Routes> - </BrowserRouter> - ) + }, [lastMessage]); + + useEffect(() => { + if (!tagManager) return; + + var _mtm = (window._mtm = window._mtm || []); + _mtm.push({ "mtm.startTime": new Date().getTime(), event: "mtm.Start" }); + var d = document, + g = d.createElement("script"), + s = d.getElementsByTagName("script")[0]; + g.async = true; + g.src = tagManager; + if (s.parentNode) { + s.parentNode.insertBefore(g, s); + } + }, [tagManager]); + + return ( + <BrowserRouter basename={routeBasename}> + <Routes> + <Route element={<QueryParamProcessor />}> + <Route element={<Layout />}> + <Route path="/" element={<Work />} /> + <Route path="/auth/magic-link/" element={<MagicLink />} /> + + <Route path="/preview/" element={<Transfer />} /> + <Route path="/work/" element={<Transfer />} /> + <Route path="/result/" element={<Result />} /> + </Route> + </Route> + </Routes> + </BrowserRouter> + ); } -export default JBApp;
\ No newline at end of file +export default JBApp; diff --git a/jb-ui/src/api_fsb/.openapi-generator/FILES b/jb-ui/src/api_fsb/.openapi-generator/FILES index d76611f..7416313 100644 --- a/jb-ui/src/api_fsb/.openapi-generator/FILES +++ b/jb-ui/src/api_fsb/.openapi-generator/FILES @@ -4,13 +4,17 @@ api.ts base.ts common.ts configuration.ts +docs/AccessType.md docs/AccountType.md docs/AdjustedAfter.md docs/AdjustedBefore.md docs/AdjustedTimestamp.md docs/AdjustmentType.md docs/AggregateBySource.md +docs/Amount.md docs/AmtCashoutMethodData.md +docs/AttemptCreditClaimBody.md +docs/AttemptCreditClaimResponse.md docs/AuditLog.md docs/AuditLogLevel.md docs/Body.md @@ -36,8 +40,10 @@ docs/ContestApi.md docs/ContestEndCondition.md docs/ContestEndConditionTargetEntryAmount.md docs/ContestEndReason.md +docs/ContestEntry.md docs/ContestEntryRule.md docs/ContestEntryTrigger.md +docs/ContestEntryType.md docs/ContestPrize.md docs/ContestPrizeKind.md docs/ContestStatus.md @@ -77,6 +83,7 @@ docs/LedgerAccount.md docs/LedgerEntry.md docs/LedgerTransaction.md docs/ListContestsUserEnteredProductIdContestUserProductUserIdEnteredGet200ResponseInner.md +docs/ManageCashoutBody.md docs/MarketProfileKnowledge.md docs/MarketplaceBucket.md docs/MarketplaceInventorySummary.md @@ -96,6 +103,7 @@ docs/OneShotOfferwall.md docs/OneShotOfferwallBucket.md docs/OneShotOfferwallResponse.md docs/OrderData.md +docs/OrderData1.md docs/POPFinancial.md docs/PatternValidation.md docs/Payload.md @@ -132,9 +140,6 @@ docs/SessionFinishPayload.md docs/SessionStatusCode2.md docs/ShippingCarrier.md docs/ShippingMethod.md -docs/SingleEntryBucket.md -docs/SingleEntryOfferWall.md -docs/SingleEntryOfferWallResponse.md docs/SoftPairBucket.md docs/SoftPairOfferwall.md docs/SoftPairOfferwallResponse.md @@ -143,8 +148,6 @@ docs/SourceConfig.md docs/SourcesConfig.md docs/StartedAfter.md docs/StartedBefore.md -docs/StarwallOfferWall.md -docs/StarwallOfferWallResponse.md docs/StarwallPlusBlockOfferWall.md docs/StarwallPlusBlockOfferWallResponse.md docs/StarwallPlusBlockRecontactOfferWall.md @@ -172,14 +175,12 @@ docs/SupplyConfig.md docs/SupplyPolicy.md docs/SurveyEligibilityCriterion.md docs/SurveyId.md +docs/THLApi.md docs/TangoCashoutMethodData.md docs/TaskEnterPayload.md docs/TaskFinishPayload.md docs/TaskStatusResponse.md docs/TasksStatusResponse.md -docs/TopNBucket.md -docs/TopNOfferWall.md -docs/TopNOfferWallResponse.md docs/TopNPlusBlockOfferWall.md docs/TopNPlusBlockOfferWallResponse.md docs/TopNPlusBlockRecontactOfferWall.md @@ -211,8 +212,10 @@ docs/Uri.md docs/User.md docs/UserAmount.md docs/UserAmountToday.md +docs/UserCpi.md docs/UserCpiString.md docs/UserCreateConfig.md +docs/UserDisplayedWalletBalance.md docs/UserHealthConfig.md docs/UserInfo.md docs/UserInfoResponse.md @@ -224,6 +227,9 @@ docs/UserLedgerTransactionUserBonus.md docs/UserLedgerTransactionUserPayout.md docs/UserLedgerTransactionsResponse.md docs/UserLedgerTransactionsResponseTransactionsInner.md +docs/UserLedgerWallet.md +docs/UserLedgerWallets.md +docs/UserPayoutEvent.md docs/UserProfile.md docs/UserProfileKnowledge.md docs/UserProfileKnowledgeAnswer.md @@ -232,7 +238,6 @@ docs/UserProfileUpdateRequest.md docs/UserProfilesResponse.md docs/UserQuestionAnswerIn.md docs/UserStreak.md -docs/UserType.md docs/UserWalletBalance.md docs/UserWalletBalanceResponse.md docs/UserWalletConfig.md diff --git a/jb-ui/src/api_fsb/api.ts b/jb-ui/src/api_fsb/api.ts index 6d0acea..d819c9c 100644 --- a/jb-ui/src/api_fsb/api.ts +++ b/jb-ui/src/api_fsb/api.ts @@ -24,6 +24,24 @@ import type { RequestArgs } from './base'; import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base'; /** + * How the network is used + * @export + * @enum {string} + */ + +export const AccessType = { + Residential: 'residential', + Mobile: 'mobile', + Satellite: 'satellite', + Datacenter: 'datacenter', + Enterprise: 'enterprise', + Unknown: 'unknown' +} as const; + +export type AccessType = typeof AccessType[keyof typeof AccessType]; + + +/** * * @export * @enum {string} @@ -33,6 +51,7 @@ export const AccountType = { BpCommission: 'bp_commission', BpWallet: 'bp_wallet', UserWallet: 'user_wallet', + UserAttemptCredit: 'user_attempt_credit', Cash: 'cash', Revenue: 'revenue', Expense: 'expense', @@ -109,6 +128,13 @@ export interface AggregateBySource { 'by_source'?: { [key: string]: number; }; } /** + * The amount of the entry in integer counts or USD Cents + * @export + * @interface Amount + */ +export interface Amount { +} +/** * * @export * @interface AmtCashoutMethodData @@ -129,6 +155,32 @@ export const AmtCashoutMethodDataTypeEnum = { export type AmtCashoutMethodDataTypeEnum = typeof AmtCashoutMethodDataTypeEnum[keyof typeof AmtCashoutMethodDataTypeEnum]; /** + * + * @export + * @interface AttemptCreditClaimBody + */ +export interface AttemptCreditClaimBody { + /** + * (product_user_id) The user requesting the claim + * @type {string} + * @memberof AttemptCreditClaimBody + */ + 'bpuid': string; +} +/** + * + * @export + * @interface AttemptCreditClaimResponse + */ +export interface AttemptCreditClaimResponse { + /** + * + * @type {number} + * @memberof AttemptCreditClaimResponse + */ + 'amount': number | null; +} +/** * Table / Model for logging \"actions\" taken by a user or \"events\" that are related to a User * @export * @interface AuditLog @@ -257,17 +309,17 @@ export interface BrokerageProductPayoutEvent { */ 'uuid'?: string; /** - * The LedgerAccount.uuid that money is being requested from. Thie User or Brokerage Product is retrievable through the LedgerAccount.reference_uuid + * * @type {string} * @memberof BrokerageProductPayoutEvent */ - 'debit_account_uuid': string; + 'debit_account_uuid': string | null; /** - * References a row in the account_cashoutmethod table. This is the cashout method that was used to request this payout. (A cashout is the same thing as a payout) + * * @type {string} * @memberof BrokerageProductPayoutEvent */ - 'cashout_method_uuid': string; + 'cashout_method_uuid': string | null; /** * * @type {string} @@ -299,11 +351,11 @@ export interface BrokerageProductPayoutEvent { */ 'payout_type': PayoutType; /** - * Stores payout-type-specific information that is used to request this payout from the external provider. + * * @type {object} * @memberof BrokerageProductPayoutEvent */ - 'request_data'?: object; + 'request_data'?: object | null; /** * * @type {OrderData} @@ -612,7 +664,7 @@ export interface CashoutMethodOut { */ 'name': string; /** - * The method in which the requested payout is delivered. Allowed values: - __PAYPAL__ *(PAYPAL)*: User is paid out to their personal PayPal email address - __TANGO__ *(TANGO)*: User is paid uut via a Tango Gift Card - __DWOLLA__ *(DWOLLA)*: DWOLLA - __ACH__ *(ACH)*: A payment is made to a bank account using ACH - __WIRE__ *(WIRE)*: A payment is made to a bank account using ACH - __CASH_IN_MAIL__ *(CASH_IN_MAIL)*: A payment is made in cash and mailed to the user. - __PRIZE__ *(PRIZE)*: A payment is made as a prize with some monetary value - __AMT__ *(AMT)*: This is used to designate either AMT_BONUS or AMT_HIT - __AMT_BONUS__ *(AMT_BONUS)*: Amazon Mechanical Turk as a Bonus - __AMT_ASSIGNMENT__ *(AMT_HIT)*: Amazon Mechanical Turk for a HIT + * The method in which the requested payout is delivered. Allowed values: - __PAYPAL__ *(PAYPAL)*: User is paid out to their personal PayPal email address - __TANGO__ *(TANGO)*: User is paid out via a Tango Gift Card - __DWOLLA__ *(DWOLLA)*: DWOLLA - __ACH__ *(ACH)*: A payment is made to a bank account using ACH - __WIRE__ *(WIRE)*: A payment is made to a bank account using ACH - __CASH_IN_MAIL__ *(CASH_IN_MAIL)*: A payment is made in cash and mailed to the user. - __PRIZE__ *(PRIZE)*: A payment is made as a prize with some monetary value - __AMT__ *(AMT)*: This is used to designate either AMT_BONUS or AMT_HIT - __AMT_BONUS__ *(AMT_BONUS)*: Amazon Mechanical Turk as a Bonus - __AMT_ASSIGNMENT__ *(AMT_HIT)*: Amazon Mechanical Turk for a HIT * @type {PayoutType} * @memberof CashoutMethodOut */ @@ -916,6 +968,57 @@ export type ContestEndReason = typeof ContestEndReason[keyof typeof ContestEndRe /** + * + * @export + * @interface ContestEntry + */ +export interface ContestEntry { + /** + * + * @type {string} + * @memberof ContestEntry + */ + 'uuid'?: string; + /** + * + * @type {string} + * @memberof ContestEntry + */ + 'created_at'?: string; + /** + * + * @type {string} + * @memberof ContestEntry + */ + 'updated_at'?: string; + /** + * + * @type {ContestEntryType} + * @memberof ContestEntry + */ + 'entry_type': ContestEntryType; + /** + * + * @type {Amount} + * @memberof ContestEntry + */ + 'amount': Amount; + /** + * + * @type {string} + * @memberof ContestEntry + */ + 'amount_str': string; + /** + * + * @type {string} + * @memberof ContestEntry + */ + 'censored_product_user_id': string; +} + + +/** * Defines rules the user must meet to be allowed to enter this contest Only applies if the ContestType is ENTRY! * @export * @interface ContestEntryRule @@ -968,6 +1071,20 @@ export type ContestEntryTrigger = typeof ContestEntryTrigger[keyof typeof Contes /** + * All entries into a contest must be of the same type, and match the entry_type of the Contest itself. + * @export + * @enum {string} + */ + +export const ContestEntryType = { + Count: 'count', + Cash: 'cash' +} as const; + +export type ContestEntryType = typeof ContestEntryType[keyof typeof ContestEntryType]; + + +/** * * @export * @interface ContestPrize @@ -1650,10 +1767,10 @@ export interface GeoIPInformation { 'static_ip_score'?: number | null; /** * - * @type {UserType} + * @type {AccessType} * @memberof GeoIPInformation */ - 'user_type'?: UserType | null; + 'user_type'?: AccessType | null; /** * * @type {string} @@ -1715,17 +1832,17 @@ export type IntegrationMode = typeof IntegrationMode[keyof typeof IntegrationMod */ export interface Leaderboard { /** - * Unique ID for this leaderboard + * * @type {string} * @memberof Leaderboard */ - 'id'?: string; + 'id'?: string | null; /** - * Descriptive name for the leaderboard based on the board_code + * * @type {string} * @memberof Leaderboard */ - 'name'?: string; + 'name'?: string | null; /** * The type of leaderboard. What the \"values\" represent. Allowed values: - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts * @type {LeaderboardCode} @@ -1751,11 +1868,11 @@ export interface Leaderboard { */ 'freq': LeaderboardFrequency; /** - * The timezone for the requested country + * * @type {string} * @memberof Leaderboard */ - 'timezone_name'?: string; + 'timezone_name'?: string | null; /** * * @type {string} @@ -1781,11 +1898,11 @@ export interface Leaderboard { */ 'period_start_local': string; /** - * The end of the time period covered by this board in local time, tz-aware + * * @type {string} * @memberof Leaderboard */ - 'period_end_local'?: string; + 'period_end_local'?: string | null; /** * (unix timestamp) The start time of the time range this leaderboard covers. * @type {number} @@ -2063,6 +2180,12 @@ export interface LeaderboardRow { * @memberof LeaderboardRow */ 'value': number; + /** + * + * @type {string} + * @memberof LeaderboardRow + */ + 'display_name'?: string | null; } /** * @@ -2197,7 +2320,7 @@ export interface LedgerAccount { */ 'qualified_name': string; /** - * - `bp_commission` = BP_COMMISSION - `bp_wallet` = BP_WALLET - `user_wallet` = USER_WALLET - `cash` = CASH - `revenue` = REVENUE - `expense` = EXPENSE - `contest_wallet` = CONTEST_WALLET - `credit_line` = CREDIT_LINE - `wa_wallet` = WA_WALLET - `wa_budget_pool` = WA_BUDGET_POOL - `wa_held` = WA_HELD - `wa_credit_line` = WA_CREDIT_LINE + * - `bp_commission` = BP_COMMISSION - `bp_wallet` = BP_WALLET - `user_wallet` = USER_WALLET - `user_attempt_credit` = USER_ATTEMPT_CREDIT - `cash` = CASH - `revenue` = REVENUE - `expense` = EXPENSE - `contest_wallet` = CONTEST_WALLET - `credit_line` = CREDIT_LINE - `wa_wallet` = WA_WALLET - `wa_budget_pool` = WA_BUDGET_POOL - `wa_held` = WA_HELD - `wa_credit_line` = WA_CREDIT_LINE * @type {AccountType} * @memberof LedgerAccount */ @@ -2545,6 +2668,27 @@ export const ListContestsUserEnteredProductIdContestUserProductUserIdEnteredGet2 export type ListContestsUserEnteredProductIdContestUserProductUserIdEnteredGet200ResponseInnerValidForEventEnum = typeof ListContestsUserEnteredProductIdContestUserProductUserIdEnteredGet200ResponseInnerValidForEventEnum[keyof typeof ListContestsUserEnteredProductIdContestUserProductUserIdEnteredGet200ResponseInnerValidForEventEnum]; /** + * + * @export + * @interface ManageCashoutBody + */ +export interface ManageCashoutBody { + /** + * + * @type {string} + * @memberof ManageCashoutBody + */ + 'payout_id': string; + /** + * + * @type {PayoutStatus} + * @memberof ManageCashoutBody + */ + 'new_status': PayoutStatus; +} + + +/** * This is used solely in API responses, so it is simplified. * @export * @interface MarketProfileKnowledge @@ -3258,6 +3402,74 @@ export const OrderDataTypeEnum = { export type OrderDataTypeEnum = typeof OrderDataTypeEnum[keyof typeof OrderDataTypeEnum]; /** + * Stores payout-type-specific order information that is returned from the external payout provider. + * @export + * @interface OrderData1 + */ +export interface OrderData1 { + /** + * + * @type {string} + * @memberof OrderData1 + */ + 'type'?: OrderData1TypeEnum; + /** + * + * @type {number} + * @memberof OrderData1 + */ + 'shipping_cost': number; + /** + * + * @type {string} + * @memberof OrderData1 + */ + 'tracking_number'?: string; + /** + * + * @type {ShippingMethod} + * @memberof OrderData1 + */ + 'shipping_method'?: ShippingMethod; + /** + * + * @type {ShippingCarrier} + * @memberof OrderData1 + */ + 'carrier'?: ShippingCarrier; + /** + * + * @type {string} + * @memberof OrderData1 + */ + 'ship_date'?: string; + /** + * + * @type {string} + * @memberof OrderData1 + */ + 'estimated_delivery_date'?: string; + /** + * + * @type {DeliveryStatus} + * @memberof OrderData1 + */ + 'delivery_status'?: DeliveryStatus; + /** + * + * @type {string} + * @memberof OrderData1 + */ + 'last_updated'?: string; +} + +export const OrderData1TypeEnum = { + CashInMail: 'CASH_IN_MAIL' +} as const; + +export type OrderData1TypeEnum = typeof OrderData1TypeEnum[keyof typeof OrderData1TypeEnum]; + +/** * We can\'t use our USDCent class in here because aside from it not supporting negative values for our adjustments, FastAPI also complains because it doesn\'t know how to generate documentation for it. - Max 2024-06-25 * @export * @interface POPFinancial @@ -3430,7 +3642,7 @@ export interface PayoutSummary { 'upper_whisker': number; } /** - * This model describe how the bp_payout is converted to the user_payout. If None, the user_payout is None. If the user_wallet_enabled is `False`, the user_payout is used to 1) know how to transform the expected payouts for offerwall buckets (if min_payout is requested, this is based on the user_payout) 2) show the user (using the payout_format) how much they made (in the Task Status Response). If the user_wallet_enabled is `True`, then in addition to the above, the user_payout is the amount actually paid to the user\'s wallet. + * This model describe how the bp_payout is converted to the user_payout. If None, the user_payout is None. If the user_wallet_enabled is `False`, the user_payout is used to 1) know how to transform the expected payouts for offerwall buckets (if min_payout is requested, this is based on the user_payout) 2) show the user (using the payout_format) how much they made (in the Task Status Response). If the user_wallet_enabled is `True`, then in addition to the above, the user_payout is the amount actually paid to the user\'s wallet. * @export * @interface PayoutTransformation */ @@ -3607,7 +3819,7 @@ export interface Product { */ 'enabled'?: boolean; /** - * This is only to determine if ACH or Wire payments should be made to the Produce. + * This is only to determine if ACH or Wire payments should be made to the Product. * @type {boolean} * @memberof Product */ @@ -3890,7 +4102,7 @@ export interface ProductBalances { */ 'available_balance_usd_str': string; /** - * + * The amount that must be held back to recover a negative Product balance. This is $0.00 when the Product balance is positive or zero; otherwise it is the absolute value of the negative balance. Business-level payout calculations use this amount to offset deficits from one Product against available funds from other Products before issuing Supplier Payments. * @type {number} * @memberof ProductBalances */ @@ -3921,7 +4133,7 @@ export interface ProfilingConfig { */ 'enabled'?: boolean; /** - * If grs_enabled is False, and is_grs is passed in the profiling-questions call, then don\'t actually return any questions. This allows a client to hit the endpoint with no limit and still get questions. In effect, this means that we\'ll redirect the user through the GRS system but won\'t present them any questions. + * If grs_enabled is False, and is_grs is passed in the profiling-questions call, then don\'t actually return any questions. This allows a client to hit the endpoint with no limit and still get questions. In effect, this means that we\'ll redirect the user through the GRS system but won\'t present them any questions. * @type {boolean} * @memberof ProfilingConfig */ @@ -3945,13 +4157,13 @@ export interface ProfilingConfig { */ 'avg_question_count'?: number; /** - * Scale how frequently we inject profiling questions, relative to the default.1 is default, 2 is twice as often. 10 means always. 0.5 half as often + * Scale how frequently we inject profiling questions, relative to the default. 1 is default, 2 is twice as often. 10 means always. 0.5 half as often * @type {number} * @memberof ProfilingConfig */ 'task_injection_freq_mult'?: number; /** - * Non-us multiplier, used to increase freq and length of profilers in all non-us countries.This value is multiplied by task_injection_freq_mult and avg_question_count. + * Non-us multiplier, used to increase freq and length of profilers in all non-us countries. This value is multiplied by task_injection_freq_mult and avg_question_count. * @type {number} * @memberof ProfilingConfig */ @@ -4736,87 +4948,6 @@ export type ShippingMethod = typeof ShippingMethod[keyof typeof ShippingMethod]; /** * * @export - * @interface SingleEntryBucket - */ -export interface SingleEntryBucket { - /** - * Unique identifier this particular bucket - * @type {string} - * @memberof SingleEntryBucket - */ - 'id': string; - /** - * The URL to send a respondent into. Must not edit this URL in any way - * @type {string} - * @memberof SingleEntryBucket - */ - 'uri': string; -} -/** - * Only returns a single bucket with the top scoring tasks. Offerwall code: `5fl8bpv5` - * @export - * @interface SingleEntryOfferWall - */ -export interface SingleEntryOfferWall { - /** - * Unique identifier to reference a generated offerwall - * @type {string} - * @memberof SingleEntryOfferWall - */ - 'id': string; - /** - * Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated. - * @type {number} - * @memberof SingleEntryOfferWall - */ - 'availability_count': number; - /** - * Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available. - * @type {number} - * @memberof SingleEntryOfferWall - */ - 'attempted_live_eligible_count'?: number; - /** - * - * @type {Array<SingleEntryBucket>} - * @memberof SingleEntryOfferWall - */ - 'buckets'?: Array<SingleEntryBucket>; - /** - * Explanations describing why so many or few opportunities are available. - * @type {Array<OfferwallReason>} - * @memberof SingleEntryOfferWall - */ - 'offerwall_reasons'?: Array<OfferwallReason>; - /** - * The format describing the str representation of a payout. Typically, this would be displayed to a user. The payout_format is similar to python format string with a subset of functionality supported. Only float with a precision are supported along with an optional comma for a thousands separator. In addition, a mathematical operator can be applied, such as dividing by 100. Examples are shown assuming payout = 100 (one dollar). - \"{payout*10:,.0f} Points\" -> \"1,000 Points\" - \"${payout/100:.2f}\" -> \"$1.00\" - * @type {string} - * @memberof SingleEntryOfferWall - */ - 'payout_format': string; -} -/** - * - * @export - * @interface SingleEntryOfferWallResponse - */ -export interface SingleEntryOfferWallResponse { - /** - * - * @type {OfferWallInfo} - * @memberof SingleEntryOfferWallResponse - */ - 'info': OfferWallInfo; - /** - * - * @type {SingleEntryOfferWall} - * @memberof SingleEntryOfferWallResponse - */ - 'offerwall': SingleEntryOfferWall; -} -/** - * - * @export * @interface SoftPairBucket */ export interface SoftPairBucket { @@ -5066,68 +5197,6 @@ export interface StartedAfter { export interface StartedBefore { } /** - * An offerwall with buckets that are clustered by setting as seeds the highest scoring surveys for each bin, then the rest are distributed according to their Euclidean distance using the bucket\'s features. Offerwall code: `b59a2d2b` - * @export - * @interface StarwallOfferWall - */ -export interface StarwallOfferWall { - /** - * Unique identifier to reference a generated offerwall - * @type {string} - * @memberof StarwallOfferWall - */ - 'id': string; - /** - * Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated. - * @type {number} - * @memberof StarwallOfferWall - */ - 'availability_count': number; - /** - * Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available. - * @type {number} - * @memberof StarwallOfferWall - */ - 'attempted_live_eligible_count'?: number; - /** - * - * @type {Array<TopNBucket>} - * @memberof StarwallOfferWall - */ - 'buckets'?: Array<TopNBucket>; - /** - * Explanations describing why so many or few opportunities are available. - * @type {Array<OfferwallReason>} - * @memberof StarwallOfferWall - */ - 'offerwall_reasons'?: Array<OfferwallReason>; - /** - * The format describing the str representation of a payout. Typically, this would be displayed to a user. The payout_format is similar to python format string with a subset of functionality supported. Only float with a precision are supported along with an optional comma for a thousands separator. In addition, a mathematical operator can be applied, such as dividing by 100. Examples are shown assuming payout = 100 (one dollar). - \"{payout*10:,.0f} Points\" -> \"1,000 Points\" - \"${payout/100:.2f}\" -> \"$1.00\" - * @type {string} - * @memberof StarwallOfferWall - */ - 'payout_format': string; -} -/** - * - * @export - * @interface StarwallOfferWallResponse - */ -export interface StarwallOfferWallResponse { - /** - * - * @type {OfferWallInfo} - * @memberof StarwallOfferWallResponse - */ - 'info': OfferWallInfo; - /** - * - * @type {StarwallOfferWall} - * @memberof StarwallOfferWallResponse - */ - 'offerwall': StarwallOfferWall; -} -/** * Same as the StarwallOfferWall, but the buckets include contents and no buckets are returned if the user is blocked. Offerwall code: `7fa1b3f4` * @export * @interface StarwallPlusBlockOfferWall @@ -5781,7 +5850,7 @@ export interface SupplyConfig { 'policies'?: Array<SupplyPolicy>; } /** - * One policy describing how GRL can interact with a marketplaces in a certain way. This is only used on the special \"global product\", and then internally in grpc logic. + * One policy describing how GRL can interact with a marketplaces in a certain way. This is only used on the special \"global product\", and then internally in grpc logic. * @export * @interface SupplyPolicy */ @@ -6299,135 +6368,6 @@ export interface TasksStatusResponse { 'pages': number | null; } /** - * - * @export - * @interface TopNBucket - */ -export interface TopNBucket { - /** - * Unique identifier this particular bucket - * @type {string} - * @memberof TopNBucket - */ - 'id': string; - /** - * The URL to send a respondent into. Must not edit this URL in any way - * @type {string} - * @memberof TopNBucket - */ - 'uri': string; - /** - * For UI. Provides a dimensionality position for the bucket on the x-axis. - * @type {number} - * @memberof TopNBucket - */ - 'x'?: number; - /** - * For UI. Provides a dimensionality position for the bucket on the y-axis. - * @type {number} - * @memberof TopNBucket - */ - 'y'?: number; - /** - * Currently unused. Will always return empty string - * @type {string} - * @memberof TopNBucket - */ - 'name'?: string; - /** - * Currently unused. Will always return empty string - * @type {string} - * @memberof TopNBucket - */ - 'description'?: string; - /** - * - * @type {Array<CategoryAssociation>} - * @memberof TopNBucket - */ - 'category'?: Array<CategoryAssociation>; - /** - * - * @type {DurationSummary} - * @memberof TopNBucket - */ - 'duration': DurationSummary; - /** - * - * @type {PayoutSummary} - * @memberof TopNBucket - */ - 'payout': PayoutSummary; - /** - * A proprietary score to determine the overall quality of the tasks that are within the bucket. Higher is better. - * @type {number} - * @memberof TopNBucket - */ - 'quality_score': number; -} -/** - * An offerwall with buckets that are clustered by the `split_by` argument using KMeans clustering. Offerwall code: `45b7228a7` - * @export - * @interface TopNOfferWall - */ -export interface TopNOfferWall { - /** - * Unique identifier to reference a generated offerwall - * @type {string} - * @memberof TopNOfferWall - */ - 'id': string; - /** - * Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated. - * @type {number} - * @memberof TopNOfferWall - */ - 'availability_count': number; - /** - * Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available. - * @type {number} - * @memberof TopNOfferWall - */ - 'attempted_live_eligible_count'?: number; - /** - * - * @type {Array<TopNBucket>} - * @memberof TopNOfferWall - */ - 'buckets'?: Array<TopNBucket>; - /** - * Explanations describing why so many or few opportunities are available. - * @type {Array<OfferwallReason>} - * @memberof TopNOfferWall - */ - 'offerwall_reasons'?: Array<OfferwallReason>; - /** - * The format describing the str representation of a payout. Typically, this would be displayed to a user. The payout_format is similar to python format string with a subset of functionality supported. Only float with a precision are supported along with an optional comma for a thousands separator. In addition, a mathematical operator can be applied, such as dividing by 100. Examples are shown assuming payout = 100 (one dollar). - \"{payout*10:,.0f} Points\" -> \"1,000 Points\" - \"${payout/100:.2f}\" -> \"$1.00\" - * @type {string} - * @memberof TopNOfferWall - */ - 'payout_format': string; -} -/** - * - * @export - * @interface TopNOfferWallResponse - */ -export interface TopNOfferWallResponse { - /** - * - * @type {OfferWallInfo} - * @memberof TopNOfferWallResponse - */ - 'info': OfferWallInfo; - /** - * - * @type {TopNOfferWall} - * @memberof TopNOfferWallResponse - */ - 'offerwall': TopNOfferWall; -} -/** * Same as the TopNOfferWall, but the buckets include contents and no buckets are returned if the user is blocked. Offerwall code: `d48cce47` * @export * @interface TopNPlusBlockOfferWall @@ -6986,7 +6926,7 @@ export interface UpkQuestion { * @type {Selector} * @memberof UpkQuestion */ - 'selector'?: Selector; + 'selector': Selector; /** * * @type {UpkQuestionConfiguration} @@ -7234,7 +7174,7 @@ export interface UpkQuestionOut { * @type {Selector} * @memberof UpkQuestionOut */ - 'selector'?: Selector; + 'selector': Selector; /** * * @type {UpkQuestionConfiguration} @@ -7526,6 +7466,13 @@ export interface UserAmount { export interface UserAmountToday { } /** + * The amount the user would earn from completing this task, if the status was a complete. If the BP has no payout xform, the user_cpi is None. This is analogous to the session\'s user_payout. + * @export + * @interface UserCpi + */ +export interface UserCpi { +} +/** * If a payout transformation is configured on this account, this is the amount to display to the user * @export * @interface UserCpiString @@ -7552,6 +7499,31 @@ export interface UserCreateConfig { 'max_hourly_create_limit'?: number | null; } /** + * Combined user-visible balance for one ledger currency. + * @export + * @interface UserDisplayedWalletBalance + */ +export interface UserDisplayedWalletBalance { + /** + * + * @type {string} + * @memberof UserDisplayedWalletBalance + */ + 'currency': string; + /** + * Displayed balance in the currency\'s smallest ledger unit. + * @type {number} + * @memberof UserDisplayedWalletBalance + */ + 'amount': number; + /** + * + * @type {string} + * @memberof UserDisplayedWalletBalance + */ + 'amount_string'?: string | null; +} +/** * * @export * @interface UserHealthConfig @@ -7685,7 +7657,7 @@ export const UserLedgerTransactionTaskAdjustmentTxTypeEnum = { export type UserLedgerTransactionTaskAdjustmentTxTypeEnum = typeof UserLedgerTransactionTaskAdjustmentTxTypeEnum[keyof typeof UserLedgerTransactionTaskAdjustmentTxTypeEnum]; /** - * In a BP with user wallet enabled, the task-complete transaction would have line items for both the credit to the bp_wallet_account and credit to user_account. This is the user-detail, so we\'ve only caring about the user\'s payment. + * In a BP with user wallet enabled, the task-complete transaction would have line items for both the credit to the bp_wallet_account and credit to user_account. This is the user-detail, so we\'ve only caring about the user\'s payment. * @export * @interface UserLedgerTransactionTaskComplete */ @@ -7996,6 +7968,181 @@ export type UserLedgerTransactionsResponseStatusEnum = typeof UserLedgerTransact export type UserLedgerTransactionsResponseTransactionsInner = { tx_type: 'bp_adjustment' } & UserLedgerTransactionTaskAdjustment | { tx_type: 'bp_payment' } & UserLedgerTransactionTaskComplete | { tx_type: 'user_bonus' } & UserLedgerTransactionUserBonus | { tx_type: 'user_payout_request' } & UserLedgerTransactionUserPayout; /** + * A user-owned ledger account exposed by the wallets endpoint. + * @export + * @interface UserLedgerWallet + */ +export interface UserLedgerWallet { + /** + * (USD cents) The amount in the user\'s wallet. + * @type {number} + * @memberof UserLedgerWallet + */ + 'amount': number; + /** + * (USD cents) The amount in the user\'s wallet this is currently redeemable. + * @type {number} + * @memberof UserLedgerWallet + */ + 'redeemable_amount': number; + /** + * + * @type {string} + * @memberof UserLedgerWallet + */ + 'payout_format'?: string | null; + /** + * + * @type {string} + * @memberof UserLedgerWallet + */ + 'amount_string'?: string | null; + /** + * + * @type {string} + * @memberof UserLedgerWallet + */ + 'redeemable_amount_string'?: string | null; + /** + * A unique identifier for this Ledger Account + * @type {string} + * @memberof UserLedgerWallet + */ + 'account_uuid': string; + /** + * + * @type {string} + * @memberof UserLedgerWallet + */ + 'account_type': UserLedgerWalletAccountTypeEnum; + /** + * + * @type {string} + * @memberof UserLedgerWallet + */ + 'currency'?: string; + /** + * Human-readable description of the Ledger Account + * @type {string} + * @memberof UserLedgerWallet + */ + 'display_name': string; +} + +export const UserLedgerWalletAccountTypeEnum = { + UserWallet: 'user_wallet', + UserAttemptCredit: 'user_attempt_credit' +} as const; + +export type UserLedgerWalletAccountTypeEnum = typeof UserLedgerWalletAccountTypeEnum[keyof typeof UserLedgerWalletAccountTypeEnum]; + +/** + * + * @export + * @interface UserLedgerWallets + */ +export interface UserLedgerWallets { + /** + * + * @type {Array<UserLedgerWallet>} + * @memberof UserLedgerWallets + */ + 'wallets'?: Array<UserLedgerWallet>; + /** + * + * @type {Array<UserDisplayedWalletBalance>} + * @memberof UserLedgerWallets + */ + 'displayed_balances'?: Array<UserDisplayedWalletBalance>; +} +/** + * A user has requested to be paid from their wallet balance. + * @export + * @interface UserPayoutEvent + */ +export interface UserPayoutEvent { + /** + * + * @type {string} + * @memberof UserPayoutEvent + */ + 'uuid'?: string; + /** + * + * @type {string} + * @memberof UserPayoutEvent + */ + 'debit_account_uuid': string | null; + /** + * + * @type {string} + * @memberof UserPayoutEvent + */ + 'cashout_method_uuid': string | null; + /** + * + * @type {string} + * @memberof UserPayoutEvent + */ + 'created'?: string; + /** + * The USDCent amount int. This cannot be 0 or negative + * @type {number} + * @memberof UserPayoutEvent + */ + 'amount': number; + /** + * + * @type {PayoutStatus} + * @memberof UserPayoutEvent + */ + 'status'?: PayoutStatus | null; + /** + * + * @type {string} + * @memberof UserPayoutEvent + */ + 'ext_ref_id'?: string | null; + /** + * - `PAYPAL` = PAYPAL - `TANGO` = TANGO - `DWOLLA` = DWOLLA - `ACH` = ACH - `WIRE` = WIRE - `CASH_IN_MAIL` = CASH_IN_MAIL - `PRIZE` = PRIZE - `AMT` = AMT - `AMT_BONUS` = AMT_BONUS - `AMT_ASSIGNMENT` = AMT_HIT + * @type {PayoutType} + * @memberof UserPayoutEvent + */ + 'payout_type': PayoutType; + /** + * + * @type {object} + * @memberof UserPayoutEvent + */ + 'request_data'?: object | null; + /** + * + * @type {OrderData1} + * @memberof UserPayoutEvent + */ + 'order_data'?: OrderData1 | null; + /** + * + * @type {string} + * @memberof UserPayoutEvent + */ + 'account_reference_type'?: string | null; + /** + * + * @type {string} + * @memberof UserPayoutEvent + */ + 'account_reference_uuid'?: string | null; + /** + * + * @type {string} + * @memberof UserPayoutEvent + */ + 'description'?: string | null; +} + + +/** * * @export * @interface UserProfile @@ -8009,6 +8156,12 @@ export interface UserProfile { 'email_address'?: string | null; /** * + * @type {string} + * @memberof UserProfile + */ + 'display_name'?: string | null; + /** + * * @type {User} * @memberof UserProfile */ @@ -8288,33 +8441,6 @@ export interface UserStreak { /** * * @export - * @enum {string} - */ - -export const UserType = { - Business: 'business', - Cafe: 'cafe', - Cellular: 'cellular', - College: 'college', - ContentDeliveryNetwork: 'content_delivery_network', - ConsumerPrivacyNetwork: 'consumer_privacy_network', - Government: 'government', - Hosting: 'hosting', - Library: 'library', - Military: 'military', - Residential: 'residential', - Router: 'router', - School: 'school', - SearchEngineSpider: 'search_engine_spider', - Traveler: 'traveler' -} as const; - -export type UserType = typeof UserType[keyof typeof UserType]; - - -/** - * - * @export * @interface UserWalletBalance */ export interface UserWalletBalance { @@ -8395,6 +8521,12 @@ export interface UserWalletConfig { */ 'enabled'?: boolean; /** + * The balance used as the user\'s displayed balance. + * @type {string} + * @memberof UserWalletConfig + */ + 'balance_type'?: UserWalletConfigBalanceTypeEnum; + /** * Uses Amazon Mechanical Turk * @type {boolean} * @memberof UserWalletConfig @@ -8402,17 +8534,31 @@ export interface UserWalletConfig { 'amt'?: boolean; /** * - * @type {Set<PayoutType>} + * @type {Array<PayoutType>} * @memberof UserWalletConfig */ - 'supported_payout_types'?: Set<PayoutType>; + 'supported_payout_types'?: Array<PayoutType>; /** * * @type {string} * @memberof UserWalletConfig */ 'min_cashout'?: string | null; + /** + * + * @type {string} + * @memberof UserWalletConfig + */ + 'failed_attempt_credit'?: string | null; } + +export const UserWalletConfigBalanceTypeEnum = { + WalletBalance: 'wallet_balance', + RedeemableBalance: 'redeemable_balance' +} as const; + +export type UserWalletConfigBalanceTypeEnum = typeof UserWalletConfigBalanceTypeEnum[keyof typeof UserWalletConfigBalanceTypeEnum]; + /** * * @export @@ -8437,6 +8583,18 @@ export interface ValidationError { * @memberof ValidationError */ 'type': string; + /** + * + * @type {any} + * @memberof ValidationError + */ + 'input'?: any; + /** + * + * @type {object} + * @memberof ValidationError + */ + 'ctx'?: object; } /** * @@ -8659,11 +8817,11 @@ export interface WallOut { */ 'adjusted_timestamp'?: AdjustedTimestamp; /** - * The amount the user would earn from completing this task, if the status was a complete. If the BP has no payout xform, the user_cpi is None. This is analogous to the session\'s user_payout. - * @type {number} + * + * @type {UserCpi} * @memberof WallOut */ - 'user_cpi'?: number; + 'user_cpi'?: UserCpi; /** * * @type {UserCpiString} @@ -9227,11 +9385,11 @@ export const LeaderboardApiAxiosParamCreator = function (configuration?: Configu * @param {string} productId The Brokerage Product ID provided by GRL. * @param {LeaderboardCode} boardCode The type of leaderboard. What the \"values\" represent. Allowed values: - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts * @param {LeaderboardFrequency} freq The time period range for the leaderboard. Allowed values: - __daily__ *(DAILY)*: UTC midnight to UTC midnight - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC - __monthly__ *(MONTHLY)*: Jan 1 00:00:00 - * @param {string} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. + * @param {string | null} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet: async (productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { + leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet: async (productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string | null, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { // verify required parameter 'productId' is not null or undefined assertParamExists('leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet', 'productId', productId) // verify required parameter 'boardCode' is not null or undefined @@ -9277,14 +9435,14 @@ export const LeaderboardApiAxiosParamCreator = function (configuration?: Configu * @param {string} productId The Brokerage Product ID provided by GRL. * @param {LeaderboardCode} boardCode The type of leaderboard. What the \"values\" represent. Allowed values: - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts * @param {LeaderboardFrequency} freq The time period range for the leaderboard. Allowed values: - __daily__ *(DAILY)*: UTC midnight to UTC midnight - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC - __monthly__ *(MONTHLY)*: Jan 1 00:00:00 - * @param {string} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. + * @param {string | null} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. * @param {string} [bpuid] Brokerage Product User ID. If passed, this results in the filtering of rows to include this user\'s row, plus rows above and below in rank (up to `limit` rows). Otherwise, the top `limit` rows are returned. * @param {string} [withinTime] Any local timestamp falling within the desired leaderboard\'s period. If not passed, uses the country\'s current time period. * @param {number} [limit] Max number of rows to return * @param {*} [options] Override http request option. * @throws {RequiredError} */ - timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet: async (productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string, bpuid?: string, withinTime?: string, limit?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { + timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet: async (productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string | null, bpuid?: string, withinTime?: string, limit?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { // verify required parameter 'productId' is not null or undefined assertParamExists('timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet', 'productId', productId) // verify required parameter 'boardCode' is not null or undefined @@ -9354,11 +9512,11 @@ export const LeaderboardApiFp = function(configuration?: Configuration) { * @param {string} productId The Brokerage Product ID provided by GRL. * @param {LeaderboardCode} boardCode The type of leaderboard. What the \"values\" represent. Allowed values: - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts * @param {LeaderboardFrequency} freq The time period range for the leaderboard. Allowed values: - __daily__ *(DAILY)*: UTC midnight to UTC midnight - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC - __monthly__ *(MONTHLY)*: Jan 1 00:00:00 - * @param {string} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. + * @param {string | null} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<LeaderboardWinnerResponse>> { + async leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string | null, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<LeaderboardWinnerResponse>> { const localVarAxiosArgs = await localVarAxiosParamCreator.leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet(productId, boardCode, freq, countryIso, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['LeaderboardApi.leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet']?.[localVarOperationServerIndex]?.url; @@ -9370,14 +9528,14 @@ export const LeaderboardApiFp = function(configuration?: Configuration) { * @param {string} productId The Brokerage Product ID provided by GRL. * @param {LeaderboardCode} boardCode The type of leaderboard. What the \"values\" represent. Allowed values: - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts * @param {LeaderboardFrequency} freq The time period range for the leaderboard. Allowed values: - __daily__ *(DAILY)*: UTC midnight to UTC midnight - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC - __monthly__ *(MONTHLY)*: Jan 1 00:00:00 - * @param {string} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. + * @param {string | null} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. * @param {string} [bpuid] Brokerage Product User ID. If passed, this results in the filtering of rows to include this user\'s row, plus rows above and below in rank (up to `limit` rows). Otherwise, the top `limit` rows are returned. * @param {string} [withinTime] Any local timestamp falling within the desired leaderboard\'s period. If not passed, uses the country\'s current time period. * @param {number} [limit] Max number of rows to return * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string, bpuid?: string, withinTime?: string, limit?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<LeaderboardResponse>> { + async timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string | null, bpuid?: string, withinTime?: string, limit?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<LeaderboardResponse>> { const localVarAxiosArgs = await localVarAxiosParamCreator.timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(productId, boardCode, freq, countryIso, bpuid, withinTime, limit, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['LeaderboardApi.timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet']?.[localVarOperationServerIndex]?.url; @@ -9399,11 +9557,11 @@ export const LeaderboardApiFactory = function (configuration?: Configuration, ba * @param {string} productId The Brokerage Product ID provided by GRL. * @param {LeaderboardCode} boardCode The type of leaderboard. What the \"values\" represent. Allowed values: - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts * @param {LeaderboardFrequency} freq The time period range for the leaderboard. Allowed values: - __daily__ *(DAILY)*: UTC midnight to UTC midnight - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC - __monthly__ *(MONTHLY)*: Jan 1 00:00:00 - * @param {string} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. + * @param {string | null} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string, options?: RawAxiosRequestConfig): AxiosPromise<LeaderboardWinnerResponse> { + leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string | null, options?: RawAxiosRequestConfig): AxiosPromise<LeaderboardWinnerResponse> { return localVarFp.leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet(productId, boardCode, freq, countryIso, options).then((request) => request(axios, basePath)); }, /** @@ -9412,14 +9570,14 @@ export const LeaderboardApiFactory = function (configuration?: Configuration, ba * @param {string} productId The Brokerage Product ID provided by GRL. * @param {LeaderboardCode} boardCode The type of leaderboard. What the \"values\" represent. Allowed values: - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts * @param {LeaderboardFrequency} freq The time period range for the leaderboard. Allowed values: - __daily__ *(DAILY)*: UTC midnight to UTC midnight - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC - __monthly__ *(MONTHLY)*: Jan 1 00:00:00 - * @param {string} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. + * @param {string | null} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. * @param {string} [bpuid] Brokerage Product User ID. If passed, this results in the filtering of rows to include this user\'s row, plus rows above and below in rank (up to `limit` rows). Otherwise, the top `limit` rows are returned. * @param {string} [withinTime] Any local timestamp falling within the desired leaderboard\'s period. If not passed, uses the country\'s current time period. * @param {number} [limit] Max number of rows to return * @param {*} [options] Override http request option. * @throws {RequiredError} */ - timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string, bpuid?: string, withinTime?: string, limit?: number, options?: RawAxiosRequestConfig): AxiosPromise<LeaderboardResponse> { + timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string | null, bpuid?: string, withinTime?: string, limit?: number, options?: RawAxiosRequestConfig): AxiosPromise<LeaderboardResponse> { return localVarFp.timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(productId, boardCode, freq, countryIso, bpuid, withinTime, limit, options).then((request) => request(axios, basePath)); }, }; @@ -9438,12 +9596,12 @@ export class LeaderboardApi extends BaseAPI { * @param {string} productId The Brokerage Product ID provided by GRL. * @param {LeaderboardCode} boardCode The type of leaderboard. What the \"values\" represent. Allowed values: - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts * @param {LeaderboardFrequency} freq The time period range for the leaderboard. Allowed values: - __daily__ *(DAILY)*: UTC midnight to UTC midnight - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC - __monthly__ *(MONTHLY)*: Jan 1 00:00:00 - * @param {string} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. + * @param {string | null} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof LeaderboardApi */ - public leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string, options?: RawAxiosRequestConfig) { + public leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string | null, options?: RawAxiosRequestConfig) { return LeaderboardApiFp(this.configuration).leaderboardWinnersProductIdLeaderboardTimespanBoardCodeWinnersGet(productId, boardCode, freq, countryIso, options).then((request) => request(this.axios, this.basePath)); } @@ -9453,7 +9611,7 @@ export class LeaderboardApi extends BaseAPI { * @param {string} productId The Brokerage Product ID provided by GRL. * @param {LeaderboardCode} boardCode The type of leaderboard. What the \"values\" represent. Allowed values: - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts * @param {LeaderboardFrequency} freq The time period range for the leaderboard. Allowed values: - __daily__ *(DAILY)*: UTC midnight to UTC midnight - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC - __monthly__ *(MONTHLY)*: Jan 1 00:00:00 - * @param {string} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. + * @param {string | null} [countryIso] Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request. * @param {string} [bpuid] Brokerage Product User ID. If passed, this results in the filtering of rows to include this user\'s row, plus rows above and below in rank (up to `limit` rows). Otherwise, the top `limit` rows are returned. * @param {string} [withinTime] Any local timestamp falling within the desired leaderboard\'s period. If not passed, uses the country\'s current time period. * @param {number} [limit] Max number of rows to return @@ -9461,7 +9619,7 @@ export class LeaderboardApi extends BaseAPI { * @throws {RequiredError} * @memberof LeaderboardApi */ - public timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string, bpuid?: string, withinTime?: string, limit?: number, options?: RawAxiosRequestConfig) { + public timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(productId: string, boardCode: LeaderboardCode, freq: LeaderboardFrequency, countryIso?: string | null, bpuid?: string, withinTime?: string, limit?: number, options?: RawAxiosRequestConfig) { return LeaderboardApiFp(this.configuration).timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(productId, boardCode, freq, countryIso, bpuid, withinTime, limit, options).then((request) => request(this.axios, this.basePath)); } } @@ -9926,168 +10084,6 @@ export const OfferwallApiAxiosParamCreator = function (configuration?: Configura }; }, /** - * Only returns a single bucket with the top scoring tasks. - * @summary Single Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - singleOfferwallProductIdOfferwall5fl8bpv5Get: async (productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, minBinSize?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { - // verify required parameter 'productId' is not null or undefined - assertParamExists('singleOfferwallProductIdOfferwall5fl8bpv5Get', 'productId', productId) - // verify required parameter 'bpuid' is not null or undefined - assertParamExists('singleOfferwallProductIdOfferwall5fl8bpv5Get', 'bpuid', bpuid) - const localVarPath = `/{product_id}/offerwall/5fl8bpv5/` - .replace(`{${"product_id"}}`, encodeURIComponent(String(productId))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - if (bpuid !== undefined) { - localVarQueryParameter['bpuid'] = bpuid; - } - - if (ip !== undefined) { - localVarQueryParameter['ip'] = ip; - } - - if (countryIso !== undefined) { - localVarQueryParameter['country_iso'] = countryIso; - } - - if (languages) { - localVarQueryParameter['languages'] = languages; - } - - if (behavior !== undefined) { - localVarQueryParameter['behavior'] = behavior; - } - - if (minPayout !== undefined) { - localVarQueryParameter['min_payout'] = minPayout; - } - - if (duration !== undefined) { - localVarQueryParameter['duration'] = duration; - } - - if (minBinSize !== undefined) { - localVarQueryParameter['min_bin_size'] = minBinSize; - } - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * Returns an offerwall with buckets that are clustered by setting as seeds the highest scoring surveys for each bin, then the rest are distributed according to their Euclidean distance using the bucket\'s features. - * @summary Starwall Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [nBins] Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum} [splitBy] Cluster tasks by payout or duration - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - starwallOfferwallProductIdOfferwallB59a2d2bGet: async (productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, nBins?: number, minBinSize?: number, splitBy?: StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { - // verify required parameter 'productId' is not null or undefined - assertParamExists('starwallOfferwallProductIdOfferwallB59a2d2bGet', 'productId', productId) - // verify required parameter 'bpuid' is not null or undefined - assertParamExists('starwallOfferwallProductIdOfferwallB59a2d2bGet', 'bpuid', bpuid) - const localVarPath = `/{product_id}/offerwall/b59a2d2b/` - .replace(`{${"product_id"}}`, encodeURIComponent(String(productId))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - if (bpuid !== undefined) { - localVarQueryParameter['bpuid'] = bpuid; - } - - if (ip !== undefined) { - localVarQueryParameter['ip'] = ip; - } - - if (countryIso !== undefined) { - localVarQueryParameter['country_iso'] = countryIso; - } - - if (languages) { - localVarQueryParameter['languages'] = languages; - } - - if (behavior !== undefined) { - localVarQueryParameter['behavior'] = behavior; - } - - if (minPayout !== undefined) { - localVarQueryParameter['min_payout'] = minPayout; - } - - if (duration !== undefined) { - localVarQueryParameter['duration'] = duration; - } - - if (nBins !== undefined) { - localVarQueryParameter['n_bins'] = nBins; - } - - if (minBinSize !== undefined) { - localVarQueryParameter['min_bin_size'] = minBinSize; - } - - if (splitBy !== undefined) { - localVarQueryParameter['split_by'] = splitBy; - } - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** * Same as the StarwallOfferWall, but the buckets include contents and no buckets are returned if the user is blocked. * @summary Starwall Plus Block Offerwall * @param {string} productId The Brokerage Product ID provided by GRL. @@ -10346,92 +10342,6 @@ export const OfferwallApiAxiosParamCreator = function (configuration?: Configura }; }, /** - * Returns an offerwall with buckets that are clustered by the `split_by` argument using KMeans clustering. - * @summary Topn Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [nBins] Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum} [splitBy] Cluster tasks by payout or duration - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - topNOfferwallProductIdOfferwall45b7228a7Get: async (productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, nBins?: number, minBinSize?: number, splitBy?: TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { - // verify required parameter 'productId' is not null or undefined - assertParamExists('topNOfferwallProductIdOfferwall45b7228a7Get', 'productId', productId) - // verify required parameter 'bpuid' is not null or undefined - assertParamExists('topNOfferwallProductIdOfferwall45b7228a7Get', 'bpuid', bpuid) - const localVarPath = `/{product_id}/offerwall/45b7228a7/` - .replace(`{${"product_id"}}`, encodeURIComponent(String(productId))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - if (bpuid !== undefined) { - localVarQueryParameter['bpuid'] = bpuid; - } - - if (ip !== undefined) { - localVarQueryParameter['ip'] = ip; - } - - if (countryIso !== undefined) { - localVarQueryParameter['country_iso'] = countryIso; - } - - if (languages) { - localVarQueryParameter['languages'] = languages; - } - - if (behavior !== undefined) { - localVarQueryParameter['behavior'] = behavior; - } - - if (minPayout !== undefined) { - localVarQueryParameter['min_payout'] = minPayout; - } - - if (duration !== undefined) { - localVarQueryParameter['duration'] = duration; - } - - if (nBins !== undefined) { - localVarQueryParameter['n_bins'] = nBins; - } - - if (minBinSize !== undefined) { - localVarQueryParameter['min_bin_size'] = minBinSize; - } - - if (splitBy !== undefined) { - localVarQueryParameter['split_by'] = splitBy; - } - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** * Same as the TopNOfferWall, but the buckets include contents and no buckets are returned if the user is blocked. * @summary Topn Plus Block Offerwall * @param {string} productId The Brokerage Product ID provided by GRL. @@ -10901,50 +10811,6 @@ export const OfferwallApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Only returns a single bucket with the top scoring tasks. - * @summary Single Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async singleOfferwallProductIdOfferwall5fl8bpv5Get(productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, minBinSize?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SingleEntryOfferWallResponse>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.singleOfferwallProductIdOfferwall5fl8bpv5Get(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, minBinSize, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['OfferwallApi.singleOfferwallProductIdOfferwall5fl8bpv5Get']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - /** - * Returns an offerwall with buckets that are clustered by setting as seeds the highest scoring surveys for each bin, then the rest are distributed according to their Euclidean distance using the bucket\'s features. - * @summary Starwall Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [nBins] Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum} [splitBy] Cluster tasks by payout or duration - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async starwallOfferwallProductIdOfferwallB59a2d2bGet(productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, nBins?: number, minBinSize?: number, splitBy?: StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StarwallOfferWallResponse>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.starwallOfferwallProductIdOfferwallB59a2d2bGet(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, nBins, minBinSize, splitBy, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['OfferwallApi.starwallOfferwallProductIdOfferwallB59a2d2bGet']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - /** * Same as the StarwallOfferWall, but the buckets include contents and no buckets are returned if the user is blocked. * @summary Starwall Plus Block Offerwall * @param {string} productId The Brokerage Product ID provided by GRL. @@ -11014,29 +10880,6 @@ export const OfferwallApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Returns an offerwall with buckets that are clustered by the `split_by` argument using KMeans clustering. - * @summary Topn Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [nBins] Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum} [splitBy] Cluster tasks by payout or duration - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async topNOfferwallProductIdOfferwall45b7228a7Get(productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, nBins?: number, minBinSize?: number, splitBy?: TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TopNOfferWallResponse>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.topNOfferwallProductIdOfferwall45b7228a7Get(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, nBins, minBinSize, splitBy, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['OfferwallApi.topNOfferwallProductIdOfferwall45b7228a7Get']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - /** * Same as the TopNOfferWall, but the buckets include contents and no buckets are returned if the user is blocked. * @summary Topn Plus Block Offerwall * @param {string} productId The Brokerage Product ID provided by GRL. @@ -11244,44 +11087,6 @@ export const OfferwallApiFactory = function (configuration?: Configuration, base return localVarFp.oneShotOfferwallProductIdOfferwall6f27b1aeGet(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, nBins, sources, options).then((request) => request(axios, basePath)); }, /** - * Only returns a single bucket with the top scoring tasks. - * @summary Single Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - singleOfferwallProductIdOfferwall5fl8bpv5Get(productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, minBinSize?: number, options?: RawAxiosRequestConfig): AxiosPromise<SingleEntryOfferWallResponse> { - return localVarFp.singleOfferwallProductIdOfferwall5fl8bpv5Get(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, minBinSize, options).then((request) => request(axios, basePath)); - }, - /** - * Returns an offerwall with buckets that are clustered by setting as seeds the highest scoring surveys for each bin, then the rest are distributed according to their Euclidean distance using the bucket\'s features. - * @summary Starwall Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [nBins] Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum} [splitBy] Cluster tasks by payout or duration - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - starwallOfferwallProductIdOfferwallB59a2d2bGet(productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, nBins?: number, minBinSize?: number, splitBy?: StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum, options?: RawAxiosRequestConfig): AxiosPromise<StarwallOfferWallResponse> { - return localVarFp.starwallOfferwallProductIdOfferwallB59a2d2bGet(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, nBins, minBinSize, splitBy, options).then((request) => request(axios, basePath)); - }, - /** * Same as the StarwallOfferWall, but the buckets include contents and no buckets are returned if the user is blocked. * @summary Starwall Plus Block Offerwall * @param {string} productId The Brokerage Product ID provided by GRL. @@ -11342,26 +11147,6 @@ export const OfferwallApiFactory = function (configuration?: Configuration, base return localVarFp.starwallPlusOfferwallProductIdOfferwall5481f322Get(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, nBins, minBinSize, splitBy, options).then((request) => request(axios, basePath)); }, /** - * Returns an offerwall with buckets that are clustered by the `split_by` argument using KMeans clustering. - * @summary Topn Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [nBins] Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum} [splitBy] Cluster tasks by payout or duration - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - topNOfferwallProductIdOfferwall45b7228a7Get(productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, nBins?: number, minBinSize?: number, splitBy?: TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum, options?: RawAxiosRequestConfig): AxiosPromise<TopNOfferWallResponse> { - return localVarFp.topNOfferwallProductIdOfferwall45b7228a7Get(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, nBins, minBinSize, splitBy, options).then((request) => request(axios, basePath)); - }, - /** * Same as the TopNOfferWall, but the buckets include contents and no buckets are returned if the user is blocked. * @summary Topn Plus Block Offerwall * @param {string} productId The Brokerage Product ID provided by GRL. @@ -11569,48 +11354,6 @@ export class OfferwallApi extends BaseAPI { } /** - * Only returns a single bucket with the top scoring tasks. - * @summary Single Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof OfferwallApi - */ - public singleOfferwallProductIdOfferwall5fl8bpv5Get(productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, minBinSize?: number, options?: RawAxiosRequestConfig) { - return OfferwallApiFp(this.configuration).singleOfferwallProductIdOfferwall5fl8bpv5Get(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, minBinSize, options).then((request) => request(this.axios, this.basePath)); - } - - /** - * Returns an offerwall with buckets that are clustered by setting as seeds the highest scoring surveys for each bin, then the rest are distributed according to their Euclidean distance using the bucket\'s features. - * @summary Starwall Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [nBins] Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum} [splitBy] Cluster tasks by payout or duration - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof OfferwallApi - */ - public starwallOfferwallProductIdOfferwallB59a2d2bGet(productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, nBins?: number, minBinSize?: number, splitBy?: StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum, options?: RawAxiosRequestConfig) { - return OfferwallApiFp(this.configuration).starwallOfferwallProductIdOfferwallB59a2d2bGet(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, nBins, minBinSize, splitBy, options).then((request) => request(this.axios, this.basePath)); - } - - /** * Same as the StarwallOfferWall, but the buckets include contents and no buckets are returned if the user is blocked. * @summary Starwall Plus Block Offerwall * @param {string} productId The Brokerage Product ID provided by GRL. @@ -11677,28 +11420,6 @@ export class OfferwallApi extends BaseAPI { } /** - * Returns an offerwall with buckets that are clustered by the `split_by` argument using KMeans clustering. - * @summary Topn Offerwall - * @param {string} productId The Brokerage Product ID provided by GRL. - * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. - * @param {string} [ip] Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. - * @param {string} [countryIso] Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) - * @param {Array<string>} [languages] Respondent\'s desired languages (ISO 639-2/B, lowercase) - * @param {string} [behavior] Allows using custom scoring functions. Please discuss directly with GRL. - * @param {string} [minPayout] Decimal representation of the minimum amount of USD that any of the tasks will pay - * @param {number} [duration] Maximum length of desired task (in seconds). - * @param {number} [nBins] Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. - * @param {number} [minBinSize] Minimum number of tasks that need to be in a bucket - * @param {TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum} [splitBy] Cluster tasks by payout or duration - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof OfferwallApi - */ - public topNOfferwallProductIdOfferwall45b7228a7Get(productId: string, bpuid: string, ip?: string, countryIso?: string, languages?: Array<string>, behavior?: string, minPayout?: string, duration?: number, nBins?: number, minBinSize?: number, splitBy?: TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum, options?: RawAxiosRequestConfig) { - return OfferwallApiFp(this.configuration).topNOfferwallProductIdOfferwall45b7228a7Get(productId, bpuid, ip, countryIso, languages, behavior, minPayout, duration, nBins, minBinSize, splitBy, options).then((request) => request(this.axios, this.basePath)); - } - - /** * Same as the TopNOfferWall, but the buckets include contents and no buckets are returned if the user is blocked. * @summary Topn Plus Block Offerwall * @param {string} productId The Brokerage Product ID provided by GRL. @@ -11788,14 +11509,6 @@ export class OfferwallApi extends BaseAPI { /** * @export */ -export const StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum = { - Payout: 'payout', - Duration: 'duration' -} as const; -export type StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum = typeof StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum[keyof typeof StarwallOfferwallProductIdOfferwallB59a2d2bGetSplitByEnum]; -/** - * @export - */ export const StarwallPlusBlockOfferwallProductIdOfferwall7fa1b3f4GetSplitByEnum = { Payout: 'payout', Duration: 'duration' @@ -11820,14 +11533,6 @@ export type StarwallPlusOfferwallProductIdOfferwall5481f322GetSplitByEnum = type /** * @export */ -export const TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum = { - Payout: 'payout', - Duration: 'duration' -} as const; -export type TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum = typeof TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum[keyof typeof TopNOfferwallProductIdOfferwall45b7228a7GetSplitByEnum]; -/** - * @export - */ export const TopNPlusBlockOfferwallProductIdOfferwallD48cce47GetSplitByEnum = { Payout: 'payout', Duration: 'duration' @@ -13149,12 +12854,162 @@ export class StreamsApi extends BaseAPI { /** + * THLApi - axios parameter creator + * @export + */ +export const THLApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Manage Cashout + * @param {ManageCashoutBody} manageCashoutBody + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + manageCashoutThlManageCashoutPost: async (manageCashoutBody: ManageCashoutBody, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { + // verify required parameter 'manageCashoutBody' is not null or undefined + assertParamExists('manageCashoutThlManageCashoutPost', 'manageCashoutBody', manageCashoutBody) + const localVarPath = `/thl/manage_cashout/`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(manageCashoutBody, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * THLApi - functional programming interface + * @export + */ +export const THLApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = THLApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Manage Cashout + * @param {ManageCashoutBody} manageCashoutBody + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async manageCashoutThlManageCashoutPost(manageCashoutBody: ManageCashoutBody, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserPayoutEvent>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.manageCashoutThlManageCashoutPost(manageCashoutBody, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['THLApi.manageCashoutThlManageCashoutPost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * THLApi - factory interface + * @export + */ +export const THLApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = THLApiFp(configuration) + return { + /** + * + * @summary Manage Cashout + * @param {ManageCashoutBody} manageCashoutBody + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + manageCashoutThlManageCashoutPost(manageCashoutBody: ManageCashoutBody, options?: RawAxiosRequestConfig): AxiosPromise<UserPayoutEvent> { + return localVarFp.manageCashoutThlManageCashoutPost(manageCashoutBody, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * THLApi - object-oriented interface + * @export + * @class THLApi + * @extends {BaseAPI} + */ +export class THLApi extends BaseAPI { + /** + * + * @summary Manage Cashout + * @param {ManageCashoutBody} manageCashoutBody + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof THLApi + */ + public manageCashoutThlManageCashoutPost(manageCashoutBody: ManageCashoutBody, options?: RawAxiosRequestConfig) { + return THLApiFp(this.configuration).manageCashoutThlManageCashoutPost(manageCashoutBody, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** * WalletApi - axios parameter creator * @export */ export const WalletApiAxiosParamCreator = function (configuration?: Configuration) { return { /** + * Attempt to claim an Attempt Credit from the user\'s latest Session. + * @summary Claim Latest Attempt Credit + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {AttemptCreditClaimBody} attemptCreditClaimBody + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + claimLatestAttemptCreditProductIdWalletAttemptCreditPost: async (productId: string, attemptCreditClaimBody: AttemptCreditClaimBody, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { + // verify required parameter 'productId' is not null or undefined + assertParamExists('claimLatestAttemptCreditProductIdWalletAttemptCreditPost', 'productId', productId) + // verify required parameter 'attemptCreditClaimBody' is not null or undefined + assertParamExists('claimLatestAttemptCreditProductIdWalletAttemptCreditPost', 'attemptCreditClaimBody', attemptCreditClaimBody) + const localVarPath = `/{product_id}/wallet/attempt-credit/` + .replace(`{${"product_id"}}`, encodeURIComponent(String(productId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(attemptCreditClaimBody, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** * A user can only have 1 unique Cashout Method of any type at a time (eg. they can\'t create multiple paypal payout methods). Cashout Methods cannot be edited, they can only be created or deleted. Creating different Cashout Methods will require type-dependent request data attributes in order to be successfully created. Currently only creating paypal or Cash in Mail cashout methods are supported * @summary Create Cashout Method * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13240,6 +13095,44 @@ export const WalletApiAxiosParamCreator = function (configuration?: Configuratio }; }, /** + * Get an Attempt Credit by a user\'s Session. + * @summary Get Attempt Credit + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {string} tsid A unique identifier for the session + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAttemptCreditProductIdWalletAttemptCreditTsidGet: async (productId: string, tsid: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { + // verify required parameter 'productId' is not null or undefined + assertParamExists('getAttemptCreditProductIdWalletAttemptCreditTsidGet', 'productId', productId) + // verify required parameter 'tsid' is not null or undefined + assertParamExists('getAttemptCreditProductIdWalletAttemptCreditTsidGet', 'tsid', tsid) + const localVarPath = `/{product_id}/wallet/attempt-credit/{tsid}/` + .replace(`{${"product_id"}}`, encodeURIComponent(String(productId))) + .replace(`{${"tsid"}}`, encodeURIComponent(String(tsid))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** * Retrieve the details for a specific Cashout event * @summary Get Cashout Detail * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13475,6 +13368,47 @@ export const WalletApiAxiosParamCreator = function (configuration?: Configuratio }; }, /** + * Retrieve a respondent\'s wallets and balances for display + * @summary Get User Wallets + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getUserWalletsProductIdWalletsGet: async (productId: string, bpuid: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { + // verify required parameter 'productId' is not null or undefined + assertParamExists('getUserWalletsProductIdWalletsGet', 'productId', productId) + // verify required parameter 'bpuid' is not null or undefined + assertParamExists('getUserWalletsProductIdWalletsGet', 'bpuid', bpuid) + const localVarPath = `/{product_id}/wallets/` + .replace(`{${"product_id"}}`, encodeURIComponent(String(productId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (bpuid !== undefined) { + localVarQueryParameter['bpuid'] = bpuid; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** * Request a cashout taken from a user\'s available wallet balance, to be paid via a particular cashout method. * @summary Submit User Cashout * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13525,6 +13459,20 @@ export const WalletApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = WalletApiAxiosParamCreator(configuration) return { /** + * Attempt to claim an Attempt Credit from the user\'s latest Session. + * @summary Claim Latest Attempt Credit + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {AttemptCreditClaimBody} attemptCreditClaimBody + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async claimLatestAttemptCreditProductIdWalletAttemptCreditPost(productId: string, attemptCreditClaimBody: AttemptCreditClaimBody, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AttemptCreditClaimResponse>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.claimLatestAttemptCreditProductIdWalletAttemptCreditPost(productId, attemptCreditClaimBody, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['WalletApi.claimLatestAttemptCreditProductIdWalletAttemptCreditPost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** * A user can only have 1 unique Cashout Method of any type at a time (eg. they can\'t create multiple paypal payout methods). Cashout Methods cannot be edited, they can only be created or deleted. Creating different Cashout Methods will require type-dependent request data attributes in order to be successfully created. Currently only creating paypal or Cash in Mail cashout methods are supported * @summary Create Cashout Method * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13554,6 +13502,20 @@ export const WalletApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** + * Get an Attempt Credit by a user\'s Session. + * @summary Get Attempt Credit + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {string} tsid A unique identifier for the session + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAttemptCreditProductIdWalletAttemptCreditTsidGet(productId: string, tsid: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AttemptCreditClaimResponse>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAttemptCreditProductIdWalletAttemptCreditTsidGet(productId, tsid, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['WalletApi.getAttemptCreditProductIdWalletAttemptCreditTsidGet']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** * Retrieve the details for a specific Cashout event * @summary Get Cashout Detail * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13630,6 +13592,20 @@ export const WalletApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** + * Retrieve a respondent\'s wallets and balances for display + * @summary Get User Wallets + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getUserWalletsProductIdWalletsGet(productId: string, bpuid: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserLedgerWallets>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getUserWalletsProductIdWalletsGet(productId, bpuid, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['WalletApi.getUserWalletsProductIdWalletsGet']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** * Request a cashout taken from a user\'s available wallet balance, to be paid via a particular cashout method. * @summary Submit User Cashout * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13654,6 +13630,17 @@ export const WalletApiFactory = function (configuration?: Configuration, basePat const localVarFp = WalletApiFp(configuration) return { /** + * Attempt to claim an Attempt Credit from the user\'s latest Session. + * @summary Claim Latest Attempt Credit + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {AttemptCreditClaimBody} attemptCreditClaimBody + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + claimLatestAttemptCreditProductIdWalletAttemptCreditPost(productId: string, attemptCreditClaimBody: AttemptCreditClaimBody, options?: RawAxiosRequestConfig): AxiosPromise<AttemptCreditClaimResponse> { + return localVarFp.claimLatestAttemptCreditProductIdWalletAttemptCreditPost(productId, attemptCreditClaimBody, options).then((request) => request(axios, basePath)); + }, + /** * A user can only have 1 unique Cashout Method of any type at a time (eg. they can\'t create multiple paypal payout methods). Cashout Methods cannot be edited, they can only be created or deleted. Creating different Cashout Methods will require type-dependent request data attributes in order to be successfully created. Currently only creating paypal or Cash in Mail cashout methods are supported * @summary Create Cashout Method * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13677,6 +13664,17 @@ export const WalletApiFactory = function (configuration?: Configuration, basePat return localVarFp.deleteCashoutMethodProductIdCashoutMethodsCashoutMethodIdDelete(productId, cashoutMethodId, bpuid, options).then((request) => request(axios, basePath)); }, /** + * Get an Attempt Credit by a user\'s Session. + * @summary Get Attempt Credit + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {string} tsid A unique identifier for the session + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAttemptCreditProductIdWalletAttemptCreditTsidGet(productId: string, tsid: string, options?: RawAxiosRequestConfig): AxiosPromise<AttemptCreditClaimResponse> { + return localVarFp.getAttemptCreditProductIdWalletAttemptCreditTsidGet(productId, tsid, options).then((request) => request(axios, basePath)); + }, + /** * Retrieve the details for a specific Cashout event * @summary Get Cashout Detail * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13738,6 +13736,17 @@ export const WalletApiFactory = function (configuration?: Configuration, basePat return localVarFp.getUserWalletBalanceProductIdWalletGet(productId, bpuid, options).then((request) => request(axios, basePath)); }, /** + * Retrieve a respondent\'s wallets and balances for display + * @summary Get User Wallets + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getUserWalletsProductIdWalletsGet(productId: string, bpuid: string, options?: RawAxiosRequestConfig): AxiosPromise<UserLedgerWallets> { + return localVarFp.getUserWalletsProductIdWalletsGet(productId, bpuid, options).then((request) => request(axios, basePath)); + }, + /** * Request a cashout taken from a user\'s available wallet balance, to be paid via a particular cashout method. * @summary Submit User Cashout * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13759,6 +13768,19 @@ export const WalletApiFactory = function (configuration?: Configuration, basePat */ export class WalletApi extends BaseAPI { /** + * Attempt to claim an Attempt Credit from the user\'s latest Session. + * @summary Claim Latest Attempt Credit + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {AttemptCreditClaimBody} attemptCreditClaimBody + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WalletApi + */ + public claimLatestAttemptCreditProductIdWalletAttemptCreditPost(productId: string, attemptCreditClaimBody: AttemptCreditClaimBody, options?: RawAxiosRequestConfig) { + return WalletApiFp(this.configuration).claimLatestAttemptCreditProductIdWalletAttemptCreditPost(productId, attemptCreditClaimBody, options).then((request) => request(this.axios, this.basePath)); + } + + /** * A user can only have 1 unique Cashout Method of any type at a time (eg. they can\'t create multiple paypal payout methods). Cashout Methods cannot be edited, they can only be created or deleted. Creating different Cashout Methods will require type-dependent request data attributes in order to be successfully created. Currently only creating paypal or Cash in Mail cashout methods are supported * @summary Create Cashout Method * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13786,6 +13808,19 @@ export class WalletApi extends BaseAPI { } /** + * Get an Attempt Credit by a user\'s Session. + * @summary Get Attempt Credit + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {string} tsid A unique identifier for the session + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WalletApi + */ + public getAttemptCreditProductIdWalletAttemptCreditTsidGet(productId: string, tsid: string, options?: RawAxiosRequestConfig) { + return WalletApiFp(this.configuration).getAttemptCreditProductIdWalletAttemptCreditTsidGet(productId, tsid, options).then((request) => request(this.axios, this.basePath)); + } + + /** * Retrieve the details for a specific Cashout event * @summary Get Cashout Detail * @param {string} productId The Brokerage Product ID provided by GRL. @@ -13857,6 +13892,19 @@ export class WalletApi extends BaseAPI { } /** + * Retrieve a respondent\'s wallets and balances for display + * @summary Get User Wallets + * @param {string} productId The Brokerage Product ID provided by GRL. + * @param {string} bpuid A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WalletApi + */ + public getUserWalletsProductIdWalletsGet(productId: string, bpuid: string, options?: RawAxiosRequestConfig) { + return WalletApiFp(this.configuration).getUserWalletsProductIdWalletsGet(productId, bpuid, options).then((request) => request(this.axios, this.basePath)); + } + + /** * Request a cashout taken from a user\'s available wallet balance, to be paid via a particular cashout method. * @summary Submit User Cashout * @param {string} productId The Brokerage Product ID provided by GRL. diff --git a/jb-ui/src/api_fsb/docs/AccessType.md b/jb-ui/src/api_fsb/docs/AccessType.md new file mode 100644 index 0000000..1e4ea7d --- /dev/null +++ b/jb-ui/src/api_fsb/docs/AccessType.md @@ -0,0 +1,19 @@ +# AccessType + +How the network is used + +## Enum + +* `Residential` (value: `'residential'`) + +* `Mobile` (value: `'mobile'`) + +* `Satellite` (value: `'satellite'`) + +* `Datacenter` (value: `'datacenter'`) + +* `Enterprise` (value: `'enterprise'`) + +* `Unknown` (value: `'unknown'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/AccountType.md b/jb-ui/src/api_fsb/docs/AccountType.md index 6d2e246..d6ae50d 100644 --- a/jb-ui/src/api_fsb/docs/AccountType.md +++ b/jb-ui/src/api_fsb/docs/AccountType.md @@ -9,6 +9,8 @@ * `UserWallet` (value: `'user_wallet'`) +* `UserAttemptCredit` (value: `'user_attempt_credit'`) + * `Cash` (value: `'cash'`) * `Revenue` (value: `'revenue'`) diff --git a/jb-ui/src/api_fsb/docs/Amount.md b/jb-ui/src/api_fsb/docs/Amount.md new file mode 100644 index 0000000..ace69cb --- /dev/null +++ b/jb-ui/src/api_fsb/docs/Amount.md @@ -0,0 +1,19 @@ +# Amount + +The amount of the entry in integer counts or USD Cents + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +## Example + +```typescript +import { Amount } from './api'; + +const instance: Amount = { +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/AttemptCreditClaimBody.md b/jb-ui/src/api_fsb/docs/AttemptCreditClaimBody.md new file mode 100644 index 0000000..acf6c7e --- /dev/null +++ b/jb-ui/src/api_fsb/docs/AttemptCreditClaimBody.md @@ -0,0 +1,20 @@ +# AttemptCreditClaimBody + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**bpuid** | **string** | (product_user_id) The user requesting the claim | [default to undefined] + +## Example + +```typescript +import { AttemptCreditClaimBody } from './api'; + +const instance: AttemptCreditClaimBody = { + bpuid, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/AttemptCreditClaimResponse.md b/jb-ui/src/api_fsb/docs/AttemptCreditClaimResponse.md new file mode 100644 index 0000000..c8d0850 --- /dev/null +++ b/jb-ui/src/api_fsb/docs/AttemptCreditClaimResponse.md @@ -0,0 +1,20 @@ +# AttemptCreditClaimResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**amount** | **number** | | [default to undefined] + +## Example + +```typescript +import { AttemptCreditClaimResponse } from './api'; + +const instance: AttemptCreditClaimResponse = { + amount, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/BrokerageProductPayoutEvent.md b/jb-ui/src/api_fsb/docs/BrokerageProductPayoutEvent.md index c2d2ca6..794932f 100644 --- a/jb-ui/src/api_fsb/docs/BrokerageProductPayoutEvent.md +++ b/jb-ui/src/api_fsb/docs/BrokerageProductPayoutEvent.md @@ -7,14 +7,14 @@ The amount - created: When the Brokerage Product was paid out Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **uuid** | **string** | | [optional] [default to undefined] -**debit_account_uuid** | **string** | The LedgerAccount.uuid that money is being requested from. Thie User or Brokerage Product is retrievable through the LedgerAccount.reference_uuid | [default to undefined] -**cashout_method_uuid** | **string** | References a row in the account_cashoutmethod table. This is the cashout method that was used to request this payout. (A cashout is the same thing as a payout) | [default to undefined] +**debit_account_uuid** | **string** | | [default to undefined] +**cashout_method_uuid** | **string** | | [default to undefined] **created** | **string** | | [optional] [default to undefined] **amount** | **number** | The USDCent amount int. This cannot be 0 or negative | [default to undefined] **status** | [**PayoutStatus**](PayoutStatus.md) | | [optional] [default to undefined] **ext_ref_id** | **string** | | [optional] [default to undefined] **payout_type** | [**PayoutType**](PayoutType.md) | - `PAYPAL` = PAYPAL - `TANGO` = TANGO - `DWOLLA` = DWOLLA - `ACH` = ACH - `WIRE` = WIRE - `CASH_IN_MAIL` = CASH_IN_MAIL - `PRIZE` = PRIZE - `AMT` = AMT - `AMT_BONUS` = AMT_BONUS - `AMT_ASSIGNMENT` = AMT_HIT | [default to undefined] -**request_data** | **object** | Stores payout-type-specific information that is used to request this payout from the external provider. | [optional] [default to undefined] +**request_data** | **object** | | [optional] [default to undefined] **order_data** | [**OrderData**](OrderData.md) | | [optional] [default to undefined] **product_id** | **string** | The Brokerage Product that was paid out | [default to undefined] **method** | [**PayoutType**](PayoutType.md) | - `PAYPAL` = PAYPAL - `TANGO` = TANGO - `DWOLLA` = DWOLLA - `ACH` = ACH - `WIRE` = WIRE - `CASH_IN_MAIL` = CASH_IN_MAIL - `PRIZE` = PRIZE - `AMT` = AMT - `AMT_BONUS` = AMT_BONUS - `AMT_ASSIGNMENT` = AMT_HIT | [readonly] [default to undefined] diff --git a/jb-ui/src/api_fsb/docs/CashoutMethodOut.md b/jb-ui/src/api_fsb/docs/CashoutMethodOut.md index c4e4832..b8c4b94 100644 --- a/jb-ui/src/api_fsb/docs/CashoutMethodOut.md +++ b/jb-ui/src/api_fsb/docs/CashoutMethodOut.md @@ -14,7 +14,7 @@ Name | Type | Description | Notes **max_value** | **number** | (In lowest unit of the original_currency), The maximum amount that can be cashed out in one transaction. | [default to undefined] **min_value** | **number** | (In lowest unit of the original_currency), The minimum amount that can be cashed out in one transaction. | [default to undefined] **name** | **string** | A descriptive name for the cashout method. | [default to undefined] -**type** | [**PayoutType**](PayoutType.md) | The method in which the requested payout is delivered. Allowed values: - __PAYPAL__ *(PAYPAL)*: User is paid out to their personal PayPal email address - __TANGO__ *(TANGO)*: User is paid uut via a Tango Gift Card - __DWOLLA__ *(DWOLLA)*: DWOLLA - __ACH__ *(ACH)*: A payment is made to a bank account using ACH - __WIRE__ *(WIRE)*: A payment is made to a bank account using ACH - __CASH_IN_MAIL__ *(CASH_IN_MAIL)*: A payment is made in cash and mailed to the user. - __PRIZE__ *(PRIZE)*: A payment is made as a prize with some monetary value - __AMT__ *(AMT)*: This is used to designate either AMT_BONUS or AMT_HIT - __AMT_BONUS__ *(AMT_BONUS)*: Amazon Mechanical Turk as a Bonus - __AMT_ASSIGNMENT__ *(AMT_HIT)*: Amazon Mechanical Turk for a HIT | [default to undefined] +**type** | [**PayoutType**](PayoutType.md) | The method in which the requested payout is delivered. Allowed values: - __PAYPAL__ *(PAYPAL)*: User is paid out to their personal PayPal email address - __TANGO__ *(TANGO)*: User is paid out via a Tango Gift Card - __DWOLLA__ *(DWOLLA)*: DWOLLA - __ACH__ *(ACH)*: A payment is made to a bank account using ACH - __WIRE__ *(WIRE)*: A payment is made to a bank account using ACH - __CASH_IN_MAIL__ *(CASH_IN_MAIL)*: A payment is made in cash and mailed to the user. - __PRIZE__ *(PRIZE)*: A payment is made as a prize with some monetary value - __AMT__ *(AMT)*: This is used to designate either AMT_BONUS or AMT_HIT - __AMT_BONUS__ *(AMT_BONUS)*: Amazon Mechanical Turk as a Bonus - __AMT_ASSIGNMENT__ *(AMT_HIT)*: Amazon Mechanical Turk for a HIT | [default to undefined] **ext_id** | **string** | | [optional] [default to undefined] **usd_exchange_rate** | **number** | | [optional] [default to undefined] **max_value_usd** | **number** | | [optional] [default to undefined] diff --git a/jb-ui/src/api_fsb/docs/ContestEntry.md b/jb-ui/src/api_fsb/docs/ContestEntry.md new file mode 100644 index 0000000..a83f01f --- /dev/null +++ b/jb-ui/src/api_fsb/docs/ContestEntry.md @@ -0,0 +1,32 @@ +# ContestEntry + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**uuid** | **string** | | [optional] [default to undefined] +**created_at** | **string** | | [optional] [default to undefined] +**updated_at** | **string** | | [optional] [default to undefined] +**entry_type** | [**ContestEntryType**](ContestEntryType.md) | | [default to undefined] +**amount** | [**Amount**](Amount.md) | | [default to undefined] +**amount_str** | **string** | | [readonly] [default to undefined] +**censored_product_user_id** | **string** | | [readonly] [default to undefined] + +## Example + +```typescript +import { ContestEntry } from './api'; + +const instance: ContestEntry = { + uuid, + created_at, + updated_at, + entry_type, + amount, + amount_str, + censored_product_user_id, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/ContestEntryType.md b/jb-ui/src/api_fsb/docs/ContestEntryType.md new file mode 100644 index 0000000..933920b --- /dev/null +++ b/jb-ui/src/api_fsb/docs/ContestEntryType.md @@ -0,0 +1,11 @@ +# ContestEntryType + +All entries into a contest must be of the same type, and match the entry_type of the Contest itself. + +## Enum + +* `Count` (value: `'count'`) + +* `Cash` (value: `'cash'`) + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/GeoIPInformation.md b/jb-ui/src/api_fsb/docs/GeoIPInformation.md index 5df598e..2066626 100644 --- a/jb-ui/src/api_fsb/docs/GeoIPInformation.md +++ b/jb-ui/src/api_fsb/docs/GeoIPInformation.md @@ -36,7 +36,7 @@ Name | Type | Description | Notes **network** | **string** | | [optional] [default to undefined] **organization** | **string** | | [optional] [default to undefined] **static_ip_score** | **number** | | [optional] [default to undefined] -**user_type** | [**UserType**](UserType.md) | | [optional] [default to undefined] +**user_type** | [**AccessType**](AccessType.md) | | [optional] [default to undefined] **postal_code** | **string** | | [optional] [default to undefined] **latitude** | **number** | | [optional] [default to undefined] **longitude** | **number** | | [optional] [default to undefined] diff --git a/jb-ui/src/api_fsb/docs/Leaderboard.md b/jb-ui/src/api_fsb/docs/Leaderboard.md index adae56c..a735f49 100644 --- a/jb-ui/src/api_fsb/docs/Leaderboard.md +++ b/jb-ui/src/api_fsb/docs/Leaderboard.md @@ -6,18 +6,18 @@ A leaderboard exists independently for each product_id in each country. Each cou Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**id** | **string** | Unique ID for this leaderboard | [optional] [default to undefined] -**name** | **string** | Descriptive name for the leaderboard based on the board_code | [optional] [default to undefined] +**id** | **string** | | [optional] [default to undefined] +**name** | **string** | | [optional] [default to undefined] **board_code** | [**LeaderboardCode**](LeaderboardCode.md) | The type of leaderboard. What the \"values\" represent. Allowed values: - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts | [default to undefined] **bpid** | **string** | product_id | [default to undefined] **country_iso** | **string** | The country this leaderboard is for. | [default to undefined] **freq** | [**LeaderboardFrequency**](LeaderboardFrequency.md) | The time period range for the leaderboard. Allowed values: - __daily__ *(DAILY)*: UTC midnight to UTC midnight - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC - __monthly__ *(MONTHLY)*: Jan 1 00:00:00 | [default to undefined] -**timezone_name** | **string** | The timezone for the requested country | [optional] [default to undefined] +**timezone_name** | **string** | | [optional] [default to undefined] **sort_order** | **string** | | [optional] [default to SortOrderEnum_Descending] **row_count** | **number** | The total number of rows in the leaderboard. | [default to undefined] **rows** | [**Array<LeaderboardRow>**](LeaderboardRow.md) | | [optional] [default to undefined] **period_start_local** | **string** | The start of the time period covered by this board in local time, tz-aware | [default to undefined] -**period_end_local** | **string** | The end of the time period covered by this board in local time, tz-aware | [optional] [default to undefined] +**period_end_local** | **string** | | [optional] [default to undefined] **start_timestamp** | **number** | (unix timestamp) The start time of the time range this leaderboard covers. | [readonly] [default to undefined] **end_timestamp** | **number** | (unix timestamp) The end time of the time range this leaderboard covers. | [readonly] [default to undefined] **utc_offset** | **string** | The UTC offset for the timezone | [readonly] [default to undefined] diff --git a/jb-ui/src/api_fsb/docs/LeaderboardRow.md b/jb-ui/src/api_fsb/docs/LeaderboardRow.md index 23ad7ab..0313f07 100644 --- a/jb-ui/src/api_fsb/docs/LeaderboardRow.md +++ b/jb-ui/src/api_fsb/docs/LeaderboardRow.md @@ -8,6 +8,7 @@ Name | Type | Description | Notes **bpuid** | **string** | product_user_id | [default to undefined] **rank** | **number** | The numerical data ranks (1 through n) of the values. Ties are ranked using the lowest rank in the group. | [default to undefined] **value** | **number** | The value. The meaning of the value is dependent on the LeaderboardCode. | [default to undefined] +**display_name** | **string** | | [optional] [default to undefined] ## Example @@ -18,6 +19,7 @@ const instance: LeaderboardRow = { bpuid, rank, value, + display_name, }; ``` diff --git a/jb-ui/src/api_fsb/docs/LedgerAccount.md b/jb-ui/src/api_fsb/docs/LedgerAccount.md index f9b1ffa..29caaba 100644 --- a/jb-ui/src/api_fsb/docs/LedgerAccount.md +++ b/jb-ui/src/api_fsb/docs/LedgerAccount.md @@ -8,7 +8,7 @@ Name | Type | Description | Notes **uuid** | **string** | A unique identifier for this Ledger Account | [optional] [default to undefined] **display_name** | **string** | Human-readable description of the Ledger Account | [default to undefined] **qualified_name** | **string** | | [default to undefined] -**account_type** | [**AccountType**](AccountType.md) | - `bp_commission` = BP_COMMISSION - `bp_wallet` = BP_WALLET - `user_wallet` = USER_WALLET - `cash` = CASH - `revenue` = REVENUE - `expense` = EXPENSE - `contest_wallet` = CONTEST_WALLET - `credit_line` = CREDIT_LINE - `wa_wallet` = WA_WALLET - `wa_budget_pool` = WA_BUDGET_POOL - `wa_held` = WA_HELD - `wa_credit_line` = WA_CREDIT_LINE | [default to undefined] +**account_type** | [**AccountType**](AccountType.md) | - `bp_commission` = BP_COMMISSION - `bp_wallet` = BP_WALLET - `user_wallet` = USER_WALLET - `user_attempt_credit` = USER_ATTEMPT_CREDIT - `cash` = CASH - `revenue` = REVENUE - `expense` = EXPENSE - `contest_wallet` = CONTEST_WALLET - `credit_line` = CREDIT_LINE - `wa_wallet` = WA_WALLET - `wa_budget_pool` = WA_BUDGET_POOL - `wa_held` = WA_HELD - `wa_credit_line` = WA_CREDIT_LINE | [default to undefined] **normal_balance** | [**Direction**](Direction.md) | - `-1` = CREDIT - `1` = DEBIT | [default to undefined] **reference_type** | **string** | | [optional] [default to undefined] **reference_uuid** | **string** | | [optional] [default to undefined] diff --git a/jb-ui/src/api_fsb/docs/ManageCashoutBody.md b/jb-ui/src/api_fsb/docs/ManageCashoutBody.md new file mode 100644 index 0000000..61fdcb2 --- /dev/null +++ b/jb-ui/src/api_fsb/docs/ManageCashoutBody.md @@ -0,0 +1,22 @@ +# ManageCashoutBody + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**payout_id** | **string** | | [default to undefined] +**new_status** | [**PayoutStatus**](PayoutStatus.md) | | [default to undefined] + +## Example + +```typescript +import { ManageCashoutBody } from './api'; + +const instance: ManageCashoutBody = { + payout_id, + new_status, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/OfferwallApi.md b/jb-ui/src/api_fsb/docs/OfferwallApi.md index 8e7591d..4b77589 100644 --- a/jb-ui/src/api_fsb/docs/OfferwallApi.md +++ b/jb-ui/src/api_fsb/docs/OfferwallApi.md @@ -10,12 +10,9 @@ All URIs are relative to *https://fsb.generalresearch.com* |[**offerwallSoftpairPostProductIdOfferwall37d1da64OfferwallIdPost**](#offerwallsoftpairpostproductidofferwall37d1da64offerwallidpost) | **POST** /{product_id}/offerwall/37d1da64/{offerwall_id}/ | Offerwall Softpair Post| |[**offerwallSoftpairProductIdOfferwall37d1da64Get**](#offerwallsoftpairproductidofferwall37d1da64get) | **GET** /{product_id}/offerwall/37d1da64/ | Offerwall Softpair| |[**oneShotOfferwallProductIdOfferwall6f27b1aeGet**](#oneshotofferwallproductidofferwall6f27b1aeget) | **GET** /{product_id}/offerwall/6f27b1ae/ | One Shot Offerwall| -|[**singleOfferwallProductIdOfferwall5fl8bpv5Get**](#singleofferwallproductidofferwall5fl8bpv5get) | **GET** /{product_id}/offerwall/5fl8bpv5/ | Single Offerwall| -|[**starwallOfferwallProductIdOfferwallB59a2d2bGet**](#starwallofferwallproductidofferwallb59a2d2bget) | **GET** /{product_id}/offerwall/b59a2d2b/ | Starwall Offerwall| |[**starwallPlusBlockOfferwallProductIdOfferwall7fa1b3f4Get**](#starwallplusblockofferwallproductidofferwall7fa1b3f4get) | **GET** /{product_id}/offerwall/7fa1b3f4/ | Starwall Plus Block Offerwall| |[**starwallPlusBlockRecontactOfferwallProductIdOfferwall630db2a4Get**](#starwallplusblockrecontactofferwallproductidofferwall630db2a4get) | **GET** /{product_id}/offerwall/630db2a4/ | Starwall Plus Block Recontact Offerwall| |[**starwallPlusOfferwallProductIdOfferwall5481f322Get**](#starwallplusofferwallproductidofferwall5481f322get) | **GET** /{product_id}/offerwall/5481f322/ | Starwall Plus Offerwall| -|[**topNOfferwallProductIdOfferwall45b7228a7Get**](#topnofferwallproductidofferwall45b7228a7get) | **GET** /{product_id}/offerwall/45b7228a7/ | Topn Offerwall| |[**topNPlusBlockOfferwallProductIdOfferwallD48cce47Get**](#topnplusblockofferwallproductidofferwalld48cce47get) | **GET** /{product_id}/offerwall/d48cce47/ | Topn Plus Block Offerwall| |[**topNPlusBlockRecontactOfferwallProductIdOfferwall1e5f0af8Get**](#topnplusblockrecontactofferwallproductidofferwall1e5f0af8get) | **GET** /{product_id}/offerwall/1e5f0af8/ | Topn Plus Block Recontact Offerwall| |[**topNPlusOfferwallProductIdOfferwallB145b803Get**](#topnplusofferwallproductidofferwallb145b803get) | **GET** /{product_id}/offerwall/b145b803/ | Topn Plus Offerwall| @@ -476,164 +473,6 @@ No authorization required [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **singleOfferwallProductIdOfferwall5fl8bpv5Get** -> SingleEntryOfferWallResponse singleOfferwallProductIdOfferwall5fl8bpv5Get() - -Only returns a single bucket with the top scoring tasks. - -### Example - -```typescript -import { - OfferwallApi, - Configuration -} from './api'; - -const configuration = new Configuration(); -const apiInstance = new OfferwallApi(configuration); - -let productId: string; //The Brokerage Product ID provided by GRL. (default to undefined) -let bpuid: string; //A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. (default to undefined) -let ip: string; //Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. (optional) (default to undefined) -let countryIso: string; //Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) (optional) (default to undefined) -let languages: Array<string>; //Respondent\'s desired languages (ISO 639-2/B, lowercase) (optional) (default to undefined) -let behavior: string; //Allows using custom scoring functions. Please discuss directly with GRL. (optional) (default to undefined) -let minPayout: string; //Decimal representation of the minimum amount of USD that any of the tasks will pay (optional) (default to undefined) -let duration: number; //Maximum length of desired task (in seconds). (optional) (default to undefined) -let minBinSize: number; //Minimum number of tasks that need to be in a bucket (optional) (default to 3) - -const { status, data } = await apiInstance.singleOfferwallProductIdOfferwall5fl8bpv5Get( - productId, - bpuid, - ip, - countryIso, - languages, - behavior, - minPayout, - duration, - minBinSize -); -``` - -### Parameters - -|Name | Type | Description | Notes| -|------------- | ------------- | ------------- | -------------| -| **productId** | [**string**] | The Brokerage Product ID provided by GRL. | defaults to undefined| -| **bpuid** | [**string**] | A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. | defaults to undefined| -| **ip** | [**string**] | Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. | (optional) defaults to undefined| -| **countryIso** | [**string**] | Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) | (optional) defaults to undefined| -| **languages** | **Array<string>** | Respondent\'s desired languages (ISO 639-2/B, lowercase) | (optional) defaults to undefined| -| **behavior** | [**string**] | Allows using custom scoring functions. Please discuss directly with GRL. | (optional) defaults to undefined| -| **minPayout** | [**string**] | Decimal representation of the minimum amount of USD that any of the tasks will pay | (optional) defaults to undefined| -| **duration** | [**number**] | Maximum length of desired task (in seconds). | (optional) defaults to undefined| -| **minBinSize** | [**number**] | Minimum number of tasks that need to be in a bucket | (optional) defaults to 3| - - -### Return type - -**SingleEntryOfferWallResponse** - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - - -### HTTP response details -| Status code | Description | Response headers | -|-------------|-------------|------------------| -|**200** | Successful Response | - | -|**422** | Validation Error | - | - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **starwallOfferwallProductIdOfferwallB59a2d2bGet** -> StarwallOfferWallResponse starwallOfferwallProductIdOfferwallB59a2d2bGet() - -Returns an offerwall with buckets that are clustered by setting as seeds the highest scoring surveys for each bin, then the rest are distributed according to their Euclidean distance using the bucket\'s features. - -### Example - -```typescript -import { - OfferwallApi, - Configuration -} from './api'; - -const configuration = new Configuration(); -const apiInstance = new OfferwallApi(configuration); - -let productId: string; //The Brokerage Product ID provided by GRL. (default to undefined) -let bpuid: string; //A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. (default to undefined) -let ip: string; //Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. (optional) (default to undefined) -let countryIso: string; //Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) (optional) (default to undefined) -let languages: Array<string>; //Respondent\'s desired languages (ISO 639-2/B, lowercase) (optional) (default to undefined) -let behavior: string; //Allows using custom scoring functions. Please discuss directly with GRL. (optional) (default to undefined) -let minPayout: string; //Decimal representation of the minimum amount of USD that any of the tasks will pay (optional) (default to undefined) -let duration: number; //Maximum length of desired task (in seconds). (optional) (default to undefined) -let nBins: number; //Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. (optional) (default to 1) -let minBinSize: number; //Minimum number of tasks that need to be in a bucket (optional) (default to 3) -let splitBy: 'payout' | 'duration'; //Cluster tasks by payout or duration (optional) (default to 'payout') - -const { status, data } = await apiInstance.starwallOfferwallProductIdOfferwallB59a2d2bGet( - productId, - bpuid, - ip, - countryIso, - languages, - behavior, - minPayout, - duration, - nBins, - minBinSize, - splitBy -); -``` - -### Parameters - -|Name | Type | Description | Notes| -|------------- | ------------- | ------------- | -------------| -| **productId** | [**string**] | The Brokerage Product ID provided by GRL. | defaults to undefined| -| **bpuid** | [**string**] | A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. | defaults to undefined| -| **ip** | [**string**] | Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. | (optional) defaults to undefined| -| **countryIso** | [**string**] | Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) | (optional) defaults to undefined| -| **languages** | **Array<string>** | Respondent\'s desired languages (ISO 639-2/B, lowercase) | (optional) defaults to undefined| -| **behavior** | [**string**] | Allows using custom scoring functions. Please discuss directly with GRL. | (optional) defaults to undefined| -| **minPayout** | [**string**] | Decimal representation of the minimum amount of USD that any of the tasks will pay | (optional) defaults to undefined| -| **duration** | [**number**] | Maximum length of desired task (in seconds). | (optional) defaults to undefined| -| **nBins** | [**number**] | Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. | (optional) defaults to 1| -| **minBinSize** | [**number**] | Minimum number of tasks that need to be in a bucket | (optional) defaults to 3| -| **splitBy** | [**'payout' | 'duration'**]**Array<'payout' | 'duration'>** | Cluster tasks by payout or duration | (optional) defaults to 'payout'| - - -### Return type - -**StarwallOfferWallResponse** - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - - -### HTTP response details -| Status code | Description | Response headers | -|-------------|-------------|------------------| -|**200** | Successful Response | - | -|**422** | Validation Error | - | - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **starwallPlusBlockOfferwallProductIdOfferwall7fa1b3f4Get** > StarwallPlusBlockOfferWallResponse starwallPlusBlockOfferwallProductIdOfferwall7fa1b3f4Get() @@ -880,88 +719,6 @@ No authorization required [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **topNOfferwallProductIdOfferwall45b7228a7Get** -> TopNOfferWallResponse topNOfferwallProductIdOfferwall45b7228a7Get() - -Returns an offerwall with buckets that are clustered by the `split_by` argument using KMeans clustering. - -### Example - -```typescript -import { - OfferwallApi, - Configuration -} from './api'; - -const configuration = new Configuration(); -const apiInstance = new OfferwallApi(configuration); - -let productId: string; //The Brokerage Product ID provided by GRL. (default to undefined) -let bpuid: string; //A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. (default to undefined) -let ip: string; //Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. (optional) (default to undefined) -let countryIso: string; //Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) (optional) (default to undefined) -let languages: Array<string>; //Respondent\'s desired languages (ISO 639-2/B, lowercase) (optional) (default to undefined) -let behavior: string; //Allows using custom scoring functions. Please discuss directly with GRL. (optional) (default to undefined) -let minPayout: string; //Decimal representation of the minimum amount of USD that any of the tasks will pay (optional) (default to undefined) -let duration: number; //Maximum length of desired task (in seconds). (optional) (default to undefined) -let nBins: number; //Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. (optional) (default to 1) -let minBinSize: number; //Minimum number of tasks that need to be in a bucket (optional) (default to 3) -let splitBy: 'payout' | 'duration'; //Cluster tasks by payout or duration (optional) (default to 'payout') - -const { status, data } = await apiInstance.topNOfferwallProductIdOfferwall45b7228a7Get( - productId, - bpuid, - ip, - countryIso, - languages, - behavior, - minPayout, - duration, - nBins, - minBinSize, - splitBy -); -``` - -### Parameters - -|Name | Type | Description | Notes| -|------------- | ------------- | ------------- | -------------| -| **productId** | [**string**] | The Brokerage Product ID provided by GRL. | defaults to undefined| -| **bpuid** | [**string**] | A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. | defaults to undefined| -| **ip** | [**string**] | Respondent\'s IP address (IPv4 or IPv6). Either \'ip\' must be provided, or \'country_iso\' must be provided if \'ip\' is not provided. | (optional) defaults to undefined| -| **countryIso** | [**string**] | Respondent\'s country code (ISO 3166-1 alpha-2, lowercase) | (optional) defaults to undefined| -| **languages** | **Array<string>** | Respondent\'s desired languages (ISO 639-2/B, lowercase) | (optional) defaults to undefined| -| **behavior** | [**string**] | Allows using custom scoring functions. Please discuss directly with GRL. | (optional) defaults to undefined| -| **minPayout** | [**string**] | Decimal representation of the minimum amount of USD that any of the tasks will pay | (optional) defaults to undefined| -| **duration** | [**number**] | Maximum length of desired task (in seconds). | (optional) defaults to undefined| -| **nBins** | [**number**] | Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all duration + payout metrics on across all (as shown by the availability_count). n_bins=0 will return back an empty bucket array, however, the availability_count will still show the total opportunities available for that bpuid + passed in parameter settings. n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split. | (optional) defaults to 1| -| **minBinSize** | [**number**] | Minimum number of tasks that need to be in a bucket | (optional) defaults to 3| -| **splitBy** | [**'payout' | 'duration'**]**Array<'payout' | 'duration'>** | Cluster tasks by payout or duration | (optional) defaults to 'payout'| - - -### Return type - -**TopNOfferWallResponse** - -### Authorization - -No authorization required - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - - -### HTTP response details -| Status code | Description | Response headers | -|-------------|-------------|------------------| -|**200** | Successful Response | - | -|**422** | Validation Error | - | - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **topNPlusBlockOfferwallProductIdOfferwallD48cce47Get** > TopNPlusBlockOfferWallResponse topNPlusBlockOfferwallProductIdOfferwallD48cce47Get() diff --git a/jb-ui/src/api_fsb/docs/OrderData1.md b/jb-ui/src/api_fsb/docs/OrderData1.md new file mode 100644 index 0000000..0cd86fa --- /dev/null +++ b/jb-ui/src/api_fsb/docs/OrderData1.md @@ -0,0 +1,37 @@ +# OrderData1 + +Stores payout-type-specific order information that is returned from the external payout provider. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**type** | **string** | | [optional] [default to TypeEnum_CashInMail] +**shipping_cost** | **number** | | [default to undefined] +**tracking_number** | **string** | | [optional] [default to undefined] +**shipping_method** | [**ShippingMethod**](ShippingMethod.md) | | [optional] [default to undefined] +**carrier** | [**ShippingCarrier**](ShippingCarrier.md) | | [optional] [default to undefined] +**ship_date** | **string** | | [optional] [default to undefined] +**estimated_delivery_date** | **string** | | [optional] [default to undefined] +**delivery_status** | [**DeliveryStatus**](DeliveryStatus.md) | | [optional] [default to undefined] +**last_updated** | **string** | | [optional] [default to undefined] + +## Example + +```typescript +import { OrderData1 } from './api'; + +const instance: OrderData1 = { + type, + shipping_cost, + tracking_number, + shipping_method, + carrier, + ship_date, + estimated_delivery_date, + delivery_status, + last_updated, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/PayoutTransformation.md b/jb-ui/src/api_fsb/docs/PayoutTransformation.md index 378fde0..fdc0d1b 100644 --- a/jb-ui/src/api_fsb/docs/PayoutTransformation.md +++ b/jb-ui/src/api_fsb/docs/PayoutTransformation.md @@ -1,6 +1,6 @@ # PayoutTransformation -This model describe how the bp_payout is converted to the user_payout. If None, the user_payout is None. If the user_wallet_enabled is `False`, the user_payout is used to 1) know how to transform the expected payouts for offerwall buckets (if min_payout is requested, this is based on the user_payout) 2) show the user (using the payout_format) how much they made (in the Task Status Response). If the user_wallet_enabled is `True`, then in addition to the above, the user_payout is the amount actually paid to the user\'s wallet. +This model describe how the bp_payout is converted to the user_payout. If None, the user_payout is None. If the user_wallet_enabled is `False`, the user_payout is used to 1) know how to transform the expected payouts for offerwall buckets (if min_payout is requested, this is based on the user_payout) 2) show the user (using the payout_format) how much they made (in the Task Status Response). If the user_wallet_enabled is `True`, then in addition to the above, the user_payout is the amount actually paid to the user\'s wallet. ## Properties diff --git a/jb-ui/src/api_fsb/docs/Product.md b/jb-ui/src/api_fsb/docs/Product.md index 3dfe1d3..c62184a 100644 --- a/jb-ui/src/api_fsb/docs/Product.md +++ b/jb-ui/src/api_fsb/docs/Product.md @@ -8,7 +8,7 @@ Name | Type | Description | Notes **id** | **string** | Unique identifier of the Brokerage Product | [optional] [default to undefined] **name** | **string** | Name of the Brokerage Product. Must be unique within a Team | [default to undefined] **enabled** | **boolean** | This is only used to hard block a Product in order to immediately & safely protect against fraud entrances. | [optional] [default to true] -**payments_enabled** | **boolean** | This is only to determine if ACH or Wire payments should be made to the Produce. | [optional] [default to true] +**payments_enabled** | **boolean** | This is only to determine if ACH or Wire payments should be made to the Product. | [optional] [default to true] **created** | **string** | | [optional] [default to undefined] **team_id** | **string** | | [optional] [default to undefined] **business_id** | **string** | | [optional] [default to undefined] diff --git a/jb-ui/src/api_fsb/docs/ProductBalances.md b/jb-ui/src/api_fsb/docs/ProductBalances.md index 92a9170..30172f2 100644 --- a/jb-ui/src/api_fsb/docs/ProductBalances.md +++ b/jb-ui/src/api_fsb/docs/ProductBalances.md @@ -27,7 +27,7 @@ Name | Type | Description | Notes **retainer_usd_str** | **string** | | [readonly] [default to undefined] **available_balance** | **number** | The Available Balance is the amount that is currently, andimmediately available for withdraw from the Supplier\'sbalance. Supplier Payments are made every Friday for Businesses with an ACH connected Bank Account to GRL, while a Business that requires an International Wire are issued on the last Friday of every Month. | [readonly] [default to undefined] **available_balance_usd_str** | **string** | | [readonly] [default to undefined] -**recoup** | **number** | | [readonly] [default to undefined] +**recoup** | **number** | The amount that must be held back to recover a negative Product balance. This is $0.00 when the Product balance is positive or zero; otherwise it is the absolute value of the negative balance. Business-level payout calculations use this amount to offset deficits from one Product against available funds from other Products before issuing Supplier Payments. | [readonly] [default to undefined] **recoup_usd_str** | **string** | | [readonly] [default to undefined] **adjustment_percent** | **number** | The percentage of USDCent value that has been adjustedover all time for this Product. | [readonly] [default to undefined] diff --git a/jb-ui/src/api_fsb/docs/ProfilingConfig.md b/jb-ui/src/api_fsb/docs/ProfilingConfig.md index 22d329d..22c2038 100644 --- a/jb-ui/src/api_fsb/docs/ProfilingConfig.md +++ b/jb-ui/src/api_fsb/docs/ProfilingConfig.md @@ -6,12 +6,12 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **enabled** | **boolean** | If False, the harmonizer/profiling system is not used at all. This should never be False unless special circumstances | [optional] [default to true] -**grs_enabled** | **boolean** | If grs_enabled is False, and is_grs is passed in the profiling-questions call, then don\'t actually return any questions. This allows a client to hit the endpoint with no limit and still get questions. In effect, this means that we\'ll redirect the user through the GRS system but won\'t present them any questions. | [optional] [default to true] +**grs_enabled** | **boolean** | If grs_enabled is False, and is_grs is passed in the profiling-questions call, then don\'t actually return any questions. This allows a client to hit the endpoint with no limit and still get questions. In effect, this means that we\'ll redirect the user through the GRS system but won\'t present them any questions. | [optional] [default to true] **n_questions** | **number** | | [optional] [default to undefined] **max_questions** | **number** | The max number of questions we would ask in a session | [optional] [default to 10] **avg_question_count** | **number** | The average number of questions to ask in a session | [optional] [default to 5] -**task_injection_freq_mult** | **number** | Scale how frequently we inject profiling questions, relative to the default.1 is default, 2 is twice as often. 10 means always. 0.5 half as often | [optional] [default to 1] -**non_us_mult** | **number** | Non-us multiplier, used to increase freq and length of profilers in all non-us countries.This value is multiplied by task_injection_freq_mult and avg_question_count. | [optional] [default to 2] +**task_injection_freq_mult** | **number** | Scale how frequently we inject profiling questions, relative to the default. 1 is default, 2 is twice as often. 10 means always. 0.5 half as often | [optional] [default to 1] +**non_us_mult** | **number** | Non-us multiplier, used to increase freq and length of profilers in all non-us countries. This value is multiplied by task_injection_freq_mult and avg_question_count. | [optional] [default to 2] **hidden_questions_expiration_hours** | **number** | How frequently we should refresh hidden questions | [optional] [default to 168] ## Example diff --git a/jb-ui/src/api_fsb/docs/SupplyPolicy.md b/jb-ui/src/api_fsb/docs/SupplyPolicy.md index 6d72fa9..8eb86eb 100644 --- a/jb-ui/src/api_fsb/docs/SupplyPolicy.md +++ b/jb-ui/src/api_fsb/docs/SupplyPolicy.md @@ -1,6 +1,6 @@ # SupplyPolicy -One policy describing how GRL can interact with a marketplaces in a certain way. This is only used on the special \"global product\", and then internally in grpc logic. +One policy describing how GRL can interact with a marketplaces in a certain way. This is only used on the special \"global product\", and then internally in grpc logic. ## Properties diff --git a/jb-ui/src/api_fsb/docs/THLApi.md b/jb-ui/src/api_fsb/docs/THLApi.md new file mode 100644 index 0000000..ed20c0a --- /dev/null +++ b/jb-ui/src/api_fsb/docs/THLApi.md @@ -0,0 +1,60 @@ +# THLApi + +All URIs are relative to *https://fsb.generalresearch.com* + +|Method | HTTP request | Description| +|------------- | ------------- | -------------| +|[**manageCashoutThlManageCashoutPost**](#managecashoutthlmanagecashoutpost) | **POST** /thl/manage_cashout/ | Manage Cashout| + +# **manageCashoutThlManageCashoutPost** +> UserPayoutEvent manageCashoutThlManageCashoutPost(manageCashoutBody) + + +### Example + +```typescript +import { + THLApi, + Configuration, + ManageCashoutBody +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new THLApi(configuration); + +let manageCashoutBody: ManageCashoutBody; // + +const { status, data } = await apiInstance.manageCashoutThlManageCashoutPost( + manageCashoutBody +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **manageCashoutBody** | **ManageCashoutBody**| | | + + +### Return type + +**UserPayoutEvent** + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Successful Response | - | +|**422** | Validation Error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/jb-ui/src/api_fsb/docs/UpkQuestion.md b/jb-ui/src/api_fsb/docs/UpkQuestion.md index 332d09f..945e91b 100644 --- a/jb-ui/src/api_fsb/docs/UpkQuestion.md +++ b/jb-ui/src/api_fsb/docs/UpkQuestion.md @@ -12,7 +12,7 @@ Name | Type | Description | Notes **language_iso** | **string** | | [default to undefined] **question_text** | **string** | The text shown to respondents | [default to undefined] **choices** | [**Array<UpkQuestionChoice>**](UpkQuestionChoice.md) | | [optional] [default to undefined] -**selector** | [**Selector**](Selector.md) | | [optional] [default to undefined] +**selector** | [**Selector**](Selector.md) | | [default to undefined] **configuration** | [**UpkQuestionConfiguration**](UpkQuestionConfiguration.md) | | [optional] [default to undefined] **validation** | [**UpkQuestionValidation**](UpkQuestionValidation.md) | | [optional] [default to undefined] **importance** | [**UPKImportance**](UPKImportance.md) | | [optional] [default to undefined] diff --git a/jb-ui/src/api_fsb/docs/UpkQuestionOut.md b/jb-ui/src/api_fsb/docs/UpkQuestionOut.md index 351ffaf..8d2f7ac 100644 --- a/jb-ui/src/api_fsb/docs/UpkQuestionOut.md +++ b/jb-ui/src/api_fsb/docs/UpkQuestionOut.md @@ -12,7 +12,7 @@ Name | Type | Description | Notes **language_iso** | **string** | | [default to undefined] **question_text** | **string** | The text shown to respondents | [default to undefined] **choices** | [**Array<UpkQuestionChoiceOut>**](UpkQuestionChoiceOut.md) | | [optional] [default to undefined] -**selector** | [**Selector**](Selector.md) | | [optional] [default to undefined] +**selector** | [**Selector**](Selector.md) | | [default to undefined] **configuration** | [**UpkQuestionConfiguration**](UpkQuestionConfiguration.md) | | [optional] [default to undefined] **validation** | [**UpkQuestionValidation**](UpkQuestionValidation.md) | | [optional] [default to undefined] **importance** | [**UPKImportance**](UPKImportance.md) | | [optional] [default to undefined] diff --git a/jb-ui/src/api_fsb/docs/UserCpi.md b/jb-ui/src/api_fsb/docs/UserCpi.md new file mode 100644 index 0000000..18048f3 --- /dev/null +++ b/jb-ui/src/api_fsb/docs/UserCpi.md @@ -0,0 +1,19 @@ +# UserCpi + + The amount the user would earn from completing this task, if the status was a complete. If the BP has no payout xform, the user_cpi is None. This is analogous to the session\'s user_payout. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +## Example + +```typescript +import { UserCpi } from './api'; + +const instance: UserCpi = { +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/UserDisplayedWalletBalance.md b/jb-ui/src/api_fsb/docs/UserDisplayedWalletBalance.md new file mode 100644 index 0000000..78c82e6 --- /dev/null +++ b/jb-ui/src/api_fsb/docs/UserDisplayedWalletBalance.md @@ -0,0 +1,25 @@ +# UserDisplayedWalletBalance + +Combined user-visible balance for one ledger currency. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**currency** | **string** | | [default to undefined] +**amount** | **number** | Displayed balance in the currency\'s smallest ledger unit. | [default to undefined] +**amount_string** | **string** | | [optional] [default to undefined] + +## Example + +```typescript +import { UserDisplayedWalletBalance } from './api'; + +const instance: UserDisplayedWalletBalance = { + currency, + amount, + amount_string, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/UserLedgerTransactionTaskComplete.md b/jb-ui/src/api_fsb/docs/UserLedgerTransactionTaskComplete.md index f513148..05cda73 100644 --- a/jb-ui/src/api_fsb/docs/UserLedgerTransactionTaskComplete.md +++ b/jb-ui/src/api_fsb/docs/UserLedgerTransactionTaskComplete.md @@ -1,6 +1,6 @@ # UserLedgerTransactionTaskComplete -In a BP with user wallet enabled, the task-complete transaction would have line items for both the credit to the bp_wallet_account and credit to user_account. This is the user-detail, so we\'ve only caring about the user\'s payment. +In a BP with user wallet enabled, the task-complete transaction would have line items for both the credit to the bp_wallet_account and credit to user_account. This is the user-detail, so we\'ve only caring about the user\'s payment. ## Properties diff --git a/jb-ui/src/api_fsb/docs/UserLedgerWallet.md b/jb-ui/src/api_fsb/docs/UserLedgerWallet.md new file mode 100644 index 0000000..d223586 --- /dev/null +++ b/jb-ui/src/api_fsb/docs/UserLedgerWallet.md @@ -0,0 +1,37 @@ +# UserLedgerWallet + +A user-owned ledger account exposed by the wallets endpoint. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**amount** | **number** | (USD cents) The amount in the user\'s wallet. | [default to undefined] +**redeemable_amount** | **number** | (USD cents) The amount in the user\'s wallet this is currently redeemable. | [default to undefined] +**payout_format** | **string** | | [optional] [default to undefined] +**amount_string** | **string** | | [optional] [default to undefined] +**redeemable_amount_string** | **string** | | [optional] [default to undefined] +**account_uuid** | **string** | A unique identifier for this Ledger Account | [default to undefined] +**account_type** | **string** | | [default to undefined] +**currency** | **string** | | [optional] [default to 'USD'] +**display_name** | **string** | Human-readable description of the Ledger Account | [default to undefined] + +## Example + +```typescript +import { UserLedgerWallet } from './api'; + +const instance: UserLedgerWallet = { + amount, + redeemable_amount, + payout_format, + amount_string, + redeemable_amount_string, + account_uuid, + account_type, + currency, + display_name, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/UserLedgerWallets.md b/jb-ui/src/api_fsb/docs/UserLedgerWallets.md new file mode 100644 index 0000000..5d202a3 --- /dev/null +++ b/jb-ui/src/api_fsb/docs/UserLedgerWallets.md @@ -0,0 +1,22 @@ +# UserLedgerWallets + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**wallets** | [**Array<UserLedgerWallet>**](UserLedgerWallet.md) | | [optional] [default to undefined] +**displayed_balances** | [**Array<UserDisplayedWalletBalance>**](UserDisplayedWalletBalance.md) | | [optional] [default to undefined] + +## Example + +```typescript +import { UserLedgerWallets } from './api'; + +const instance: UserLedgerWallets = { + wallets, + displayed_balances, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/UserPayoutEvent.md b/jb-ui/src/api_fsb/docs/UserPayoutEvent.md new file mode 100644 index 0000000..02b62ab --- /dev/null +++ b/jb-ui/src/api_fsb/docs/UserPayoutEvent.md @@ -0,0 +1,45 @@ +# UserPayoutEvent + +A user has requested to be paid from their wallet balance. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**uuid** | **string** | | [optional] [default to undefined] +**debit_account_uuid** | **string** | | [default to undefined] +**cashout_method_uuid** | **string** | | [default to undefined] +**created** | **string** | | [optional] [default to undefined] +**amount** | **number** | The USDCent amount int. This cannot be 0 or negative | [default to undefined] +**status** | [**PayoutStatus**](PayoutStatus.md) | | [optional] [default to undefined] +**ext_ref_id** | **string** | | [optional] [default to undefined] +**payout_type** | [**PayoutType**](PayoutType.md) | - `PAYPAL` = PAYPAL - `TANGO` = TANGO - `DWOLLA` = DWOLLA - `ACH` = ACH - `WIRE` = WIRE - `CASH_IN_MAIL` = CASH_IN_MAIL - `PRIZE` = PRIZE - `AMT` = AMT - `AMT_BONUS` = AMT_BONUS - `AMT_ASSIGNMENT` = AMT_HIT | [default to undefined] +**request_data** | **object** | | [optional] [default to undefined] +**order_data** | [**OrderData1**](OrderData1.md) | | [optional] [default to undefined] +**account_reference_type** | **string** | | [optional] [default to undefined] +**account_reference_uuid** | **string** | | [optional] [default to undefined] +**description** | **string** | | [optional] [default to undefined] + +## Example + +```typescript +import { UserPayoutEvent } from './api'; + +const instance: UserPayoutEvent = { + uuid, + debit_account_uuid, + cashout_method_uuid, + created, + amount, + status, + ext_ref_id, + payout_type, + request_data, + order_data, + account_reference_type, + account_reference_uuid, + description, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jb-ui/src/api_fsb/docs/UserProfile.md b/jb-ui/src/api_fsb/docs/UserProfile.md index 7bacbdf..843cb50 100644 --- a/jb-ui/src/api_fsb/docs/UserProfile.md +++ b/jb-ui/src/api_fsb/docs/UserProfile.md @@ -6,6 +6,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **email_address** | **string** | | [optional] [default to undefined] +**display_name** | **string** | | [optional] [default to undefined] **user** | [**User**](User.md) | | [default to undefined] **marketplace_pids** | **{ [key: string]: string; }** | User\'s PID in marketplaces | [optional] [default to undefined] **streaks** | [**Array<UserStreak>**](UserStreak.md) | | [optional] [default to undefined] @@ -20,6 +21,7 @@ import { UserProfile } from './api'; const instance: UserProfile = { email_address, + display_name, user, marketplace_pids, streaks, diff --git a/jb-ui/src/api_fsb/docs/UserWalletConfig.md b/jb-ui/src/api_fsb/docs/UserWalletConfig.md index 96d9587..2d9ef41 100644 --- a/jb-ui/src/api_fsb/docs/UserWalletConfig.md +++ b/jb-ui/src/api_fsb/docs/UserWalletConfig.md @@ -7,9 +7,11 @@ Stores configuration for the user wallet handling Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **enabled** | **boolean** | If enabled, the users\' wallets are managed. | [optional] [default to false] +**balance_type** | **string** | The balance used as the user\'s displayed balance. | [optional] [default to BalanceTypeEnum_WalletBalance] **amt** | **boolean** | Uses Amazon Mechanical Turk | [optional] [default to false] -**supported_payout_types** | [**Set<PayoutType>**](PayoutType.md) | | [optional] [default to undefined] +**supported_payout_types** | [**Array<PayoutType>**](PayoutType.md) | | [optional] [default to undefined] **min_cashout** | **string** | | [optional] [default to undefined] +**failed_attempt_credit** | **string** | | [optional] [default to undefined] ## Example @@ -18,9 +20,11 @@ import { UserWalletConfig } from './api'; const instance: UserWalletConfig = { enabled, + balance_type, amt, supported_payout_types, min_cashout, + failed_attempt_credit, }; ``` diff --git a/jb-ui/src/api_fsb/docs/ValidationError.md b/jb-ui/src/api_fsb/docs/ValidationError.md index d2e7ec1..912f97f 100644 --- a/jb-ui/src/api_fsb/docs/ValidationError.md +++ b/jb-ui/src/api_fsb/docs/ValidationError.md @@ -8,6 +8,8 @@ Name | Type | Description | Notes **loc** | [**Array<ValidationErrorLocInner>**](ValidationErrorLocInner.md) | | [default to undefined] **msg** | **string** | | [default to undefined] **type** | **string** | | [default to undefined] +**input** | **any** | | [optional] [default to undefined] +**ctx** | **object** | | [optional] [default to undefined] ## Example @@ -18,6 +20,8 @@ const instance: ValidationError = { loc, msg, type, + input, + ctx, }; ``` diff --git a/jb-ui/src/api_fsb/docs/WallOut.md b/jb-ui/src/api_fsb/docs/WallOut.md index aca3aef..89830a1 100644 --- a/jb-ui/src/api_fsb/docs/WallOut.md +++ b/jb-ui/src/api_fsb/docs/WallOut.md @@ -22,7 +22,7 @@ Name | Type | Description | Notes **report_notes** | [**ReportNotes**](ReportNotes.md) | | [optional] [default to undefined] **adjusted_status** | [**WallOutAdjustedStatus**](WallOutAdjustedStatus.md) | | [optional] [default to undefined] **adjusted_timestamp** | [**AdjustedTimestamp**](AdjustedTimestamp.md) | | [optional] [default to undefined] -**user_cpi** | **number** | The amount the user would earn from completing this task, if the status was a complete. If the BP has no payout xform, the user_cpi is None. This is analogous to the session\'s user_payout. | [optional] [default to undefined] +**user_cpi** | [**UserCpi**](UserCpi.md) | | [optional] [default to undefined] **user_cpi_string** | [**UserCpiString**](UserCpiString.md) | | [optional] [default to undefined] ## Example diff --git a/jb-ui/src/api_fsb/docs/WalletApi.md b/jb-ui/src/api_fsb/docs/WalletApi.md index 80bd3ed..a4acfd4 100644 --- a/jb-ui/src/api_fsb/docs/WalletApi.md +++ b/jb-ui/src/api_fsb/docs/WalletApi.md @@ -4,15 +4,74 @@ All URIs are relative to *https://fsb.generalresearch.com* |Method | HTTP request | Description| |------------- | ------------- | -------------| +|[**claimLatestAttemptCreditProductIdWalletAttemptCreditPost**](#claimlatestattemptcreditproductidwalletattemptcreditpost) | **POST** /{product_id}/wallet/attempt-credit/ | Claim Latest Attempt Credit| |[**createCashoutMethodProductIdCashoutMethodsPost**](#createcashoutmethodproductidcashoutmethodspost) | **POST** /{product_id}/cashout_methods/ | Create Cashout Method| |[**deleteCashoutMethodProductIdCashoutMethodsCashoutMethodIdDelete**](#deletecashoutmethodproductidcashoutmethodscashoutmethodiddelete) | **DELETE** /{product_id}/cashout_methods/{cashout_method_id}/ | Delete Cashout Method| +|[**getAttemptCreditProductIdWalletAttemptCreditTsidGet**](#getattemptcreditproductidwalletattemptcredittsidget) | **GET** /{product_id}/wallet/attempt-credit/{tsid}/ | Get Attempt Credit| |[**getCashoutDetailProductIdCashoutCashoutIdGet**](#getcashoutdetailproductidcashoutcashoutidget) | **GET** /{product_id}/cashout/{cashout_id}/ | Get Cashout Detail| |[**getCashoutMethodsProductIdCashoutMethodsGet**](#getcashoutmethodsproductidcashoutmethodsget) | **GET** /{product_id}/cashout_methods/ | Get Cashout Methods| |[**getExpectedRedemptionValueProductIdCashoutMethodCashoutMethodIdValueGet**](#getexpectedredemptionvalueproductidcashoutmethodcashoutmethodidvalueget) | **GET** /{product_id}/cashout_method/{cashout_method_id}/value/ | Get Expected Redemption Value| |[**getUserTransactionHistoryProductIdTransactionHistoryGet**](#getusertransactionhistoryproductidtransactionhistoryget) | **GET** /{product_id}/transaction_history/ | Get User Transaction History| |[**getUserWalletBalanceProductIdWalletGet**](#getuserwalletbalanceproductidwalletget) | **GET** /{product_id}/wallet/ | Get User Wallet Balance| +|[**getUserWalletsProductIdWalletsGet**](#getuserwalletsproductidwalletsget) | **GET** /{product_id}/wallets/ | Get User Wallets| |[**submitUserCashoutProductIdCashoutPost**](#submitusercashoutproductidcashoutpost) | **POST** /{product_id}/cashout/ | Submit User Cashout| +# **claimLatestAttemptCreditProductIdWalletAttemptCreditPost** +> AttemptCreditClaimResponse claimLatestAttemptCreditProductIdWalletAttemptCreditPost(attemptCreditClaimBody) + +Attempt to claim an Attempt Credit from the user\'s latest Session. + +### Example + +```typescript +import { + WalletApi, + Configuration, + AttemptCreditClaimBody +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new WalletApi(configuration); + +let productId: string; //The Brokerage Product ID provided by GRL. (default to undefined) +let attemptCreditClaimBody: AttemptCreditClaimBody; // + +const { status, data } = await apiInstance.claimLatestAttemptCreditProductIdWalletAttemptCreditPost( + productId, + attemptCreditClaimBody +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **attemptCreditClaimBody** | **AttemptCreditClaimBody**| | | +| **productId** | [**string**] | The Brokerage Product ID provided by GRL. | defaults to undefined| + + +### Return type + +**AttemptCreditClaimResponse** + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Successful Response | - | +|**422** | Validation Error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **createCashoutMethodProductIdCashoutMethodsPost** > CashoutMethodResponse createCashoutMethodProductIdCashoutMethodsPost(body) @@ -127,6 +186,61 @@ No authorization required [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **getAttemptCreditProductIdWalletAttemptCreditTsidGet** +> AttemptCreditClaimResponse getAttemptCreditProductIdWalletAttemptCreditTsidGet() + +Get an Attempt Credit by a user\'s Session. + +### Example + +```typescript +import { + WalletApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new WalletApi(configuration); + +let productId: string; //The Brokerage Product ID provided by GRL. (default to undefined) +let tsid: string; //A unique identifier for the session (default to undefined) + +const { status, data } = await apiInstance.getAttemptCreditProductIdWalletAttemptCreditTsidGet( + productId, + tsid +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **productId** | [**string**] | The Brokerage Product ID provided by GRL. | defaults to undefined| +| **tsid** | [**string**] | A unique identifier for the session | defaults to undefined| + + +### Return type + +**AttemptCreditClaimResponse** + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Successful Response | - | +|**422** | Validation Error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **getCashoutDetailProductIdCashoutCashoutIdGet** > CashoutRequestResponse getCashoutDetailProductIdCashoutCashoutIdGet() @@ -420,6 +534,61 @@ No authorization required [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **getUserWalletsProductIdWalletsGet** +> UserLedgerWallets getUserWalletsProductIdWalletsGet() + +Retrieve a respondent\'s wallets and balances for display + +### Example + +```typescript +import { + WalletApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new WalletApi(configuration); + +let productId: string; //The Brokerage Product ID provided by GRL. (default to undefined) +let bpuid: string; //A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. (default to undefined) + +const { status, data } = await apiInstance.getUserWalletsProductIdWalletsGet( + productId, + bpuid +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **productId** | [**string**] | The Brokerage Product ID provided by GRL. | defaults to undefined| +| **bpuid** | [**string**] | A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values. | defaults to undefined| + + +### Return type + +**UserLedgerWallets** + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Successful Response | - | +|**422** | Validation Error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **submitUserCashoutProductIdCashoutPost** > CashoutRequestResponse submitUserCashoutProductIdCashoutPost(createCashoutRequest) diff --git a/jb-ui/src/api_fsb/openapi.json b/jb-ui/src/api_fsb/openapi.json index 8e9e5db..318e6cb 100644 --- a/jb-ui/src/api_fsb/openapi.json +++ b/jb-ui/src/api_fsb/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"General Research Full Service Brokerage API","version":"1.2.0"},"servers":[{"url":"https://fsb.generalresearch.com/","description":"Production environment"}],"paths":{"/{product_id}/offerwall/5fl8bpv5/":{"get":{"tags":["Offerwall"],"summary":"Single Offerwall","description":"Only returns a single bucket with the top scoring tasks.","operationId":"Single_Offerwall__product_id__offerwall_5fl8bpv5__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SingleEntryOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/45b7228a7/":{"get":{"tags":["Offerwall"],"summary":"Topn Offerwall","description":"Returns an offerwall with buckets that are clustered by the `split_by` argument\nusing KMeans clustering.","operationId":"TopN_Offerwall__product_id__offerwall_45b7228a7__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":1,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopNOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/b59a2d2b/":{"get":{"tags":["Offerwall"],"summary":"Starwall Offerwall","description":"Returns an offerwall with buckets that are clustered by setting as seeds the\nhighest scoring surveys for each bin, then the rest are distributed\naccording to their Euclidean distance using the bucket's features.","operationId":"Starwall_Offerwall__product_id__offerwall_b59a2d2b__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":1,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StarwallOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/b145b803/":{"get":{"tags":["Offerwall"],"summary":"Topn Plus Offerwall","description":"Same as the TopNOfferWall, but the buckets include contents.","operationId":"TopN_Plus_Offerwall__product_id__offerwall_b145b803__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopNPlusOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/d48cce47/":{"get":{"tags":["Offerwall"],"summary":"Topn Plus Block Offerwall","description":"Same as the TopNOfferWall, but the buckets include contents and no\nbuckets are returned if the user is blocked.","operationId":"TopN_Plus_Block_Offerwall__product_id__offerwall_d48cce47__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopNPlusBlockOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/1e5f0af8/":{"get":{"tags":["Offerwall"],"summary":"Topn Plus Block Recontact Offerwall","description":"Same as the TopNOfferWall, but the buckets include contents, no buckets\nare returned if the user is blocked, and each bucket includes a\n`is_recontact` key.","operationId":"TopN_Plus_Block_Recontact_Offerwall__product_id__offerwall_1e5f0af8__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopNPlusBlockRecontactOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/5481f322/":{"get":{"tags":["Offerwall"],"summary":"Starwall Plus Offerwall","description":"Same as the StarwallOfferWall, but the buckets include contents.","operationId":"Starwall_Plus_Offerwall__product_id__offerwall_5481f322__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StarwallPlusOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/7fa1b3f4/":{"get":{"tags":["Offerwall"],"summary":"Starwall Plus Block Offerwall","description":"Same as the StarwallOfferWall, but the buckets include contents and no\nbuckets are returned if the user is blocked.","operationId":"Starwall_Plus_Block_Offerwall__product_id__offerwall_7fa1b3f4__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StarwallPlusBlockOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/630db2a4/":{"get":{"tags":["Offerwall"],"summary":"Starwall Plus Block Recontact Offerwall","description":"Same as the StarwallOfferWall, but the buckets include contents, no buckets\nare returned if the user is blocked, and each bucket includes a\n`is_recontact` key.","operationId":"Starwall_Plus_Block_Recontact_Offerwall__product_id__offerwall_630db2a4__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StarwallPlusBlockRecontactOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/5fa23085/":{"get":{"tags":["Offerwall"],"summary":"Marketplace Offerwall","description":"Returns buckets grouped by marketplace, one per marketplace, with the\ntasks ordered by quality.","operationId":"Marketplace_Offerwall__product_id__offerwall_5fa23085__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketplaceOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/6f27b1ae/":{"get":{"tags":["Offerwall"],"summary":"One Shot Offerwall","description":"Each bucket has only 1 single task, and only basic info is returned\n about each bucket.","operationId":"One_Shot_Offerwall__product_id__offerwall_6f27b1ae__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":8,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"sources","in":"query","required":false,"schema":{"type":"array","uniqueItems":true,"items":{"$ref":"#/components/schemas/Source"},"description":"Restrict tasks to those from these marketplaces only.","title":"Sources"},"description":"Restrict tasks to those from these marketplaces only."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OneShotOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/55a4e1a9/":{"get":{"tags":["Offerwall"],"summary":"Wxet Offerwall","description":"Each bucket has only 1 single task, and only basic info is returned\n about each bucket.","operationId":"WXET_Offerwall__product_id__offerwall_55a4e1a9__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":8,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WXETOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/37d1da64/":{"get":{"tags":["Offerwall"],"summary":"Offerwall Softpair","description":"This offerwall contains tasks for which the user has a conditional\neligibility. The questions that a user must answer to determine the\neligibility are included within each bucket. Additionally, the question\ndefinitions are included for convenience.","operationId":"offerwall_softpair__product_id__offerwall_37d1da64__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":12,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":1,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"max_options","in":"query","required":false,"schema":{"type":"integer","description":"Max number of options an allowed question can have (allowed to be asked)","default":40,"title":"Max Options"},"description":"Max number of options an allowed question can have (allowed to be asked)"},{"name":"max_questions","in":"query","required":false,"schema":{"type":"integer","description":"Max number of missing questions on a single bin","default":3,"title":"Max Questions"},"description":"Max number of missing questions on a single bin"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SoftPairOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/37d1da64/{offerwall_id}/":{"post":{"tags":["Offerwall"],"summary":"Offerwall Softpair Post","description":"This is a custom endpoint for a softpair-style offerwall that allows\nyou to update profiling answers and then immediately get back the\nupdated offerwall","operationId":"offerwall_softpair_post__product_id__offerwall_37d1da64__offerwall_id___post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"offerwall_id","in":"path","required":true,"schema":{"type":"string","description":"The offerwall ID the user is interacting with","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"],"title":"Offerwall Id"},"description":"The offerwall ID the user is interacting with"},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"session_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"A session id (uuid4.hex)","examples":["dd6aa32cfe834839a80195563f528337"],"title":"Session Id"},"description":"A session id (uuid4.hex)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_offerwall_softpair_post__product_id__offerwall_37d1da64__offerwall_id___post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SoftPairOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/7a89dcdb/":{"get":{"tags":["Offerwall"],"summary":"Offerwall Softpair Block","description":"This offerwall contains tasks for which the user has a conditional\neligibility. The questions that a user must answer to determine the\neligibility are included within each bucket. Additionally, the question\ndefinitions are included for convenience.\nNo buckets are returned if the user is blocked.","operationId":"offerwall_softpair_block__product_id__offerwall_7a89dcdb__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":12,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":1,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"max_options","in":"query","required":false,"schema":{"type":"integer","description":"Max number of options an allowed question can have (allowed to be asked)","default":40,"title":"Max Options"},"description":"Max number of options an allowed question can have (allowed to be asked)"},{"name":"max_questions","in":"query","required":false,"schema":{"type":"integer","description":"Max number of missing questions on a single bin","default":3,"title":"Max Questions"},"description":"Max number of missing questions on a single bin"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SoftPairOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/7a89dcdb/{offerwall_id}/":{"post":{"tags":["Offerwall"],"summary":"Offerwall Softpair Block Post","description":"This is a custom endpoint for a softpair-style offerwall that allows\nyou to update profiling answers and then immediately get back the\nupdated offerwall","operationId":"offerwall_softpair_block_post__product_id__offerwall_7a89dcdb__offerwall_id___post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"offerwall_id","in":"path","required":true,"schema":{"type":"string","description":"The offerwall ID the user is interacting with","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"],"title":"Offerwall Id"},"description":"The offerwall ID the user is interacting with"},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"session_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"A session id (uuid4.hex)","examples":["dd6aa32cfe834839a80195563f528337"],"title":"Session Id"},"description":"A session id (uuid4.hex)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_offerwall_softpair_block_post__product_id__offerwall_7a89dcdb__offerwall_id___post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SoftPairOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/status/{tsid}/":{"get":{"tags":["Status"],"summary":"Get Task Status","description":"Retrieve the status of a session by a specific Task Status ID (`tsid`)\nthat is provided in the redirect.\n\nGiven a `tsid`, get back the status of the wall event. This endpoint\nconflates the idea of a wall event and a session, but for these\npurposes, the status and payout of the last wall event in a session\nequals the status and payout for the session.","operationId":"Get_Task_Status__product_id__status__tsid___get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"tsid","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Task Status ID","description":"A unique identifier for the session","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"description":"A unique identifier for the session"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskStatusResponse"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusInfoResponseFail"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusInfoResponseFail"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusInfoResponseFail"}}},"description":"Internal Server Error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/status/":{"get":{"tags":["Status"],"summary":"List Task Statuses","operationId":"List_Task_Statuses__product_id__status__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":false,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Filters the returned task status to only include those done by this user.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Filters the returned task status to only include those done by this user."},{"name":"started_after","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","format":"date-time"}],"description":"Retrieve sessions which were started after this timestamp. If `NULL`, default is 10 days ago.","examples":["2026-02-17T00:46:04.812196Z",1771202765],"title":"Started After"},"description":"Retrieve sessions which were started after this timestamp. If `NULL`, default is 10 days ago."},{"name":"started_before","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","format":"date-time"}],"description":"Retrieve sessions which were started before this timestamp. If `NULL`, default is now.","title":"Started Before"},"description":"Retrieve sessions which were started before this timestamp. If `NULL`, default is now."},{"name":"status","in":"query","required":false,"schema":{"type":"integer","description":"Filter for sessions with this status. See Task Status for description of status.","examples":[3],"title":"Status"},"description":"Filter for sessions with this status. See Task Status for description of status."},{"name":"reconciled","in":"query","required":false,"schema":{"type":"boolean","description":"Only retrieve sessions that have been adjusted. If `reconciled` is True, `adjusted_after` is required.","default":false,"title":"Reconciled"},"description":"Only retrieve sessions that have been adjusted. If `reconciled` is True, `adjusted_after` is required."},{"name":"adjusted_after","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","format":"date-time"}],"description":"Retrieve sessions which were adjusted after this timestamp.","title":"Adjusted After"},"description":"Retrieve sessions which were adjusted after this timestamp."},{"name":"adjusted_before","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","format":"date-time"}],"description":"Retrieve sessions which were adjusted before this timestamp. Default is now.","title":"Adjusted Before"},"description":"Retrieve sessions which were adjusted before this timestamp. Default is now."},{"name":"order_by","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated fields to order by","examples":["-started","started"],"default":"-started","title":"Order By"},"description":"Comma-separated fields to order by"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TasksStatusResponse"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponseFailure"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponseFailure"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponseFailure"}}},"description":"Internal Server Error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/report/":{"post":{"tags":["Report"],"summary":"Report Task","description":"Send a \"Task Report\" for a respondent's latest started survey,\nregardless of the survey's outcome. Task Reports cannot be provided for\ntask attempts by tsid, or through any other method, they must be given\nimmediately after the respondent's task attempt.\n\nIf a latest attempt can't be found, a ReportTaskResponse is still returned.\n\nIf multiple reasons want to be provided, they should be done at the\nsame time. If multiple reports are given for the same latest started\ntask, the note attribute will be overwritten, while the new unique\nreasons will be appended to the task attempt.\n\nTask Report system contributions by client applications are immediately\nleveraged to aid yield management strategies. For those reasons, it's\nimportant that the respondent submits a Task Report as soon as possible\nand applications doesn't offload this request for a later time. Task\nReport timestamps are a critical component of understanding a Task\nexperience for a respondent.\n\nFor the BP to tell us about a bad survey they want to report\nThe POSTed data looks like:\n {\n 'bpuid': '1234',\n 'reasons': [2],\n 'notes': \"This survey was the worst!\"\n }\nReturns {'status': 'success', 'msg': error_msg}","operationId":"report_task__product_id__report__post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTask"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/profiling-questions/":{"get":{"tags":["Profiling Questions"],"summary":"Get Profiling Questions","description":"Gets a list of profiling questions to ask this user. These questions\nare, as of right now, unanswered. Once a question is answered, it\n(and any other question whose answer is inferred from other answers) is\nremoved from the list. The questions returned are ordered by\nimportance (the key task_score) in descending order.","operationId":"get_profiling_questions__product_id__profiling_questions__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"language_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{3}$","description":"Respondent's desired language (ISO 639-2/B, lowercase)","examples":["eng"],"title":"Language Iso"},"description":"Respondent's desired language (ISO 639-2/B, lowercase)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","exclusiveMinimum":0,"description":"Number of questions to return. Questions are ordered, so, selects the N most important. If no limit is passed, then the number of questions returned is dynamically calculated by the questions' importance.","title":"Limit"},"description":"Number of questions to return. Questions are ordered, so, selects the N most important. If no limit is passed, then the number of questions returned is dynamically calculated by the questions' importance."},{"name":"is_grs","in":"query","required":false,"schema":{"type":"boolean","description":"If it's the GRS system.","default":false,"title":"Is Grs"},"description":"If it's the GRS system."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpkQuestionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Profiling Questions"],"summary":"Submit Profiling Questions","description":"Send the answers to one or more of these questions for a user. A\nquestion is uniquely specified by the question_id key. The answer is:\nthe choice_id if the question_type is \"MC\" the actual entered text if\nthe question_type is \"TE\"","operationId":"submit_profiling_questions__product_id__profiling_questions__post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"session_id","in":"query","required":false,"schema":{"type":"string","description":"The unique identifier for a session in which these questions were asked. Must be a valid UUID. Optional.","title":"Session Id"},"description":"The unique identifier for a session in which these questions were asked. Must be a valid UUID. Optional."},{"name":"async","in":"query","required":false,"schema":{"type":"boolean","description":"For internal use.","default":true,"title":"Async"},"description":"For internal use."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_submit_profiling_questions__product_id__profiling_questions__post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/profiling-info/":{"get":{"tags":["Profiling Questions"],"summary":"Profiling Info","description":"Get UPK Ontology (list of properties, property info, and allowed values)","operationId":"profiling_info__product_id__profiling_info__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"country_iso","in":"query","required":true,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"language_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{3}$","description":"Respondent's desired language (ISO 639-2/B, lowercase)","examples":["eng"],"title":"Language Iso"},"description":"Respondent's desired language (ISO 639-2/B, lowercase)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfilingInfoResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/user-profile/":{"get":{"tags":["Profiling Questions"],"summary":"User Profile","operationId":"user_profile__product_id__user_profile__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"country_iso","in":"query","required":true,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"language_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{3}$","description":"Respondent's desired language (ISO 639-2/B, lowercase)","examples":["eng"],"title":"Language Iso"},"description":"Respondent's desired language (ISO 639-2/B, lowercase)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfoResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/user/{product_user_id}/profile/":{"get":{"tags":["Product User"],"summary":"User Profile","operationId":"user_profile__product_id__user__product_user_id__profile__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Product User"],"summary":"User Profile Update","operationId":"user_profile_update__product_id__user__product_user_id__profile__patch","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfileUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/user/by-marketplace-pid/":{"get":{"tags":["Product User"],"summary":"User Profile Filter By Marketplace Pid","operationId":"user_profile_filter_by_marketplace_pid__product_id__user_by_marketplace_pid__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"source","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Source","description":"The marketplace to search"},"description":"The marketplace to search"},{"name":"pid","in":"query","required":true,"schema":{"type":"array","items":{"type":"string","minLength":32,"maxLength":32},"description":"The user's marketplace PID. Supports multiple params","title":"Pid"},"description":"The user's marketplace PID. Supports multiple params"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfilesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/leaderboard/timespan/{board_code}/":{"get":{"tags":["Leaderboard"],"summary":"Timespan Leaderboard","operationId":"Timespan_Leaderboard__product_id__leaderboard_timespan__board_code___get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"board_code","in":"path","required":true,"schema":{"$ref":"#/components/schemas/LeaderboardCode","description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts"},"description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts"},{"name":"freq","in":"query","required":true,"schema":{"$ref":"#/components/schemas/LeaderboardFrequency","description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00"},"description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00"},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request.","examples":["us"],"title":"Country Iso"},"description":"Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request."},{"name":"bpuid","in":"query","required":false,"schema":{"type":"string","minLength":3,"maxLength":128,"description":"Brokerage Product User ID. If passed, this results in the filtering of rows to include this user's row, plus rows above and below in rank (up to `limit` rows). Otherwise, the top `limit` rows are returned.","examples":["app-user-9329ebd"],"title":"Bpuid"},"description":"Brokerage Product User ID. If passed, this results in the filtering of rows to include this user's row, plus rows above and below in rank (up to `limit` rows). Otherwise, the top `limit` rows are returned."},{"name":"within_time","in":"query","required":false,"schema":{"type":"string","format":"date-time","description":"Any local timestamp falling within the desired leaderboard's period. If not passed, uses the country's current time period.","examples":["2024-02-27","2024-02-27T15:30:00"],"title":"Within Time"},"description":"Any local timestamp falling within the desired leaderboard's period. If not passed, uses the country's current time period."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","exclusiveMaximum":5000,"exclusiveMinimum":0,"description":"Max number of rows to return","default":50,"title":"Limit"},"description":"Max number of rows to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/leaderboard/timespan/{board_code}/winners/":{"get":{"tags":["Leaderboard"],"summary":"Leaderboard Winners","description":"Get the Winners of leaderboard contests","operationId":"Leaderboard_Winners__product_id__leaderboard_timespan__board_code__winners__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"board_code","in":"path","required":true,"schema":{"$ref":"#/components/schemas/LeaderboardCode","description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts"},"description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts"},{"name":"freq","in":"query","required":true,"schema":{"$ref":"#/components/schemas/LeaderboardFrequency","description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00"},"description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00"},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request.","examples":["us"],"title":"Country Iso"},"description":"Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardWinnerResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/contest/{contest_uuid}/user/{product_user_id}/":{"get":{"tags":["Contest"],"summary":"Get Contest User View","description":"Gets a Contest. Includes several keys that are specific to the requesting user:\n e.g. user_winnings, user_amount.","operationId":"Get_Contest_User_View__product_id__contest__contest_uuid__user__product_user_id___get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"contest_uuid","in":"path","required":true,"schema":{"type":"string","title":"Contest Uuid"}},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RaffleUserView"},{"$ref":"#/components/schemas/LeaderboardContestUserView"},{"$ref":"#/components/schemas/MilestoneUserView"}],"title":"Response Get Contest User View Product Id Contest Contest Uuid User Product User Id Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/contest/user/{product_user_id}/entered/":{"get":{"tags":["Contest"],"summary":"List Contests User Entered","description":"Get contests the requesting user has entered. This includes contests such as milestone\n contests where the user was automatically entered by having a complete for instance.","operationId":"List_Contests_User_Entered__product_id__contest_user__product_user_id__entered__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/RaffleUserView"},{"$ref":"#/components/schemas/LeaderboardContestUserView"},{"$ref":"#/components/schemas/MilestoneUserView"}]},"title":"Response List Contests User Entered Product Id Contest User Product User Id Entered Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/contest/user/{product_user_id}/eligible/":{"get":{"tags":["Contest"],"summary":"List Contests User Eligible","description":"Get contests the requesting user is eligible for.\nContest eligibility may involve the user's country, which is determined by the ip address\n of THIS request.","operationId":"List_Contests_User_Eligible__product_id__contest_user__product_user_id__eligible__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/RaffleUserView"},{"$ref":"#/components/schemas/LeaderboardContestUserView"},{"$ref":"#/components/schemas/MilestoneUserView"}]},"title":"Response List Contests User Eligible Product Id Contest User Product User Id Eligible Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/contest/user/{product_user_id}/winner/":{"get":{"tags":["Contest"],"summary":"List Contests User Winner","description":"Get contests the requesting user has won a prize for.","operationId":"List_Contests_User_Winner__product_id__contest_user__product_user_id__winner__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/RaffleUserView"},{"$ref":"#/components/schemas/LeaderboardContestUserView"},{"$ref":"#/components/schemas/MilestoneUserView"}]},"title":"Response List Contests User Winner Product Id Contest User Product User Id Winner Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/wallet/":{"get":{"tags":["Wallet"],"summary":"Get User Wallet Balance","description":"Retrieve the current balance of a respondent's wallet balance for display","operationId":"get_user_wallet_balance__product_id__wallet__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserWalletBalanceResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/cashout_methods/":{"get":{"tags":["Wallet"],"summary":"Get Cashout Methods","description":"Retrieve the current available mechanisms that a user can use to retrieve funds.","operationId":"get_cashout_methods__product_id__cashout_methods__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CashoutMethodsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Wallet"],"summary":"Create Cashout Method","description":"A user can only have 1 unique Cashout Method of any type at a time (eg. they can't create multiple paypal\npayout methods). Cashout Methods cannot be edited, they can only be created or deleted. Creating different\nCashout Methods will require type-dependent request data attributes in order to be successfully created.\n\nCurrently only creating paypal or Cash in Mail cashout methods are supported","operationId":"create_cashout_method__product_id__cashout_methods__post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CreatePayPalCashoutMethodRequest"},{"$ref":"#/components/schemas/CreateCashMailCashoutMethodRequest"}],"discriminator":{"propertyName":"type","mapping":{"PAYPAL":"#/components/schemas/CreatePayPalCashoutMethodRequest","CASH_IN_MAIL":"#/components/schemas/CreateCashMailCashoutMethodRequest"}},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CashoutMethodResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/cashout_methods/{cashout_method_id}/":{"delete":{"tags":["Wallet"],"summary":"Delete Cashout Method","description":"Delete a cashout method","operationId":"delete_cashout_method__product_id__cashout_methods__cashout_method_id___delete","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"cashout_method_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"description":"The ID of the cashout method to delete","title":"Cashout Method Id"},"description":"The ID of the cashout method to delete"},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/cashout/":{"post":{"tags":["Wallet"],"summary":"Submit User Cashout","description":"Request a cashout taken from a user's available wallet balance, to be paid via a particular\ncashout method.","operationId":"submit_user_cashout__product_id__cashout__post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCashoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CashoutRequestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/cashout/{cashout_id}/":{"get":{"tags":["Wallet"],"summary":"Get Cashout Detail","description":"Retrieve the details for a specific Cashout event","operationId":"get_cashout_detail__product_id__cashout__cashout_id___get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"cashout_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"description":"The cashout event ID","title":"Cashout Id"},"description":"The cashout event ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CashoutRequestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/transaction_history/":{"get":{"tags":["Wallet"],"summary":"Get User Transaction History","description":"Return array of user events that altered their wallet balance","operationId":"get_user_transaction_history__product_id__transaction_history__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"created_after","in":"query","required":false,"schema":{"type":"string","format":"date-time","description":"Filter transactions created after this timestamp","examples":["2025-01-01T04:20:00.000000Z"],"title":"Created After"},"description":"Filter transactions created after this timestamp"},{"name":"created_before","in":"query","required":false,"schema":{"type":"string","format":"date-time","description":"Filter transactions created before this timestamp","examples":["2026-02-17T00:46:04.851591Z"],"title":"Created Before"},"description":"Filter transactions created before this timestamp"},{"name":"order_by","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated fields to order by","examples":["-created","created"],"default":"created","title":"Order By"},"description":"Comma-separated fields to order by"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLedgerTransactionsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/cashout_method/{cashout_method_id}/value/":{"get":{"tags":["Wallet"],"summary":"Get Expected Redemption Value","description":"Use this function to get the \"value\" (in local currency, e.g. CAD, BTC) for this\n cashout_method, for amount (in USD cents).","operationId":"get_expected_redemption_value__product_id__cashout_method__cashout_method_id__value__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"cashout_method_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"description":"The ID of the cashout method","examples":["0d363971792249b1a34586f092819e4b"],"title":"Cashout Method Id"},"description":"The ID of the cashout method"},{"name":"amount","in":"query","required":true,"schema":{"type":"integer","description":"(USD cents) The amount to convert","examples":[100],"title":"Amount"},"description":"(USD cents) The amount to convert"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CashoutMethodForeignValueResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/inventory/":{"get":{"tags":["Dashboard"],"summary":"Marketplace Inventory","operationId":"marketplace_inventory_dashboard_inventory__get","parameters":[{"name":"marketplace","in":"query","required":true,"schema":{"const":"WXET","type":"string","examples":["WXET"],"title":"Marketplace"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketplaceSummary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ws/debug/":{"get":{"tags":["Streams"],"summary":"Websocket Debug Page","operationId":"websocket_debug_page_ws_debug__get","parameters":[{"name":"product_id","in":"query","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Product Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ws/events/docs/":{"get":{"tags":["Streams"],"summary":"Websocket Events Docs","description":"WebSocket endpoint: `/ws/events/`\n\nProtocol:\n- Client connects\n- Client sends a SubscribeMessage `{ kind: \"subscribe\", product_id: \"{product_id}\" }`\n- Server sends a PingMessage `{ kind: \"ping\" }`\n- Client must reply a PongMessage: `{ kind: \"pong\" }`\n- Other messages are EventMessage or StatsMessage\n\nClose on missing pong after 20s.","operationId":"websocket_events_docs_ws_events_docs__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/EventMessage"},{"$ref":"#/components/schemas/StatsMessage"},{"$ref":"#/components/schemas/PingMessage"},{"$ref":"#/components/schemas/SubscribeMessage"},{"$ref":"#/components/schemas/PongMessage"}],"title":"Response Websocket Events Docs Ws Events Docs Get"}}}}}}}},"components":{"schemas":{"AccountType":{"type":"string","enum":["bp_commission","bp_wallet","user_wallet","cash","revenue","expense","contest_wallet","credit_line","wa_wallet","wa_budget_pool","wa_held","wa_credit_line"],"title":"AccountType"},"AdjustmentType":{"properties":{"amount":{"type":"integer","title":"Amount","description":"The total amount (in USD cents) that the Brokerage Producthas earned within a respective time period from a specificSource of Tasks."},"adjustment":{"$ref":"#/components/schemas/SessionAdjustedStatus","description":" - `ac` = ADJUSTED_TO_COMPLETE\n - `af` = ADJUSTED_TO_FAIL\n - `pa` = PAYOUT_ADJUSTMENT","examples":["af"]}},"type":"object","required":["amount","adjustment"],"title":"AdjustmentType"},"AggregateBySource":{"properties":{"total":{"type":"integer","minimum":0.0,"title":"Total","default":0},"by_source":{"additionalProperties":{"type":"integer","minimum":0.0},"propertyNames":{"$ref":"#/components/schemas/Source"},"type":"object","title":"By Source"}},"type":"object","title":"AggregateBySource"},"AmtCashoutMethodData":{"properties":{"type":{"type":"string","const":"AMT","title":"Type","default":"AMT"}},"type":"object","title":"AmtCashoutMethodData"},"AuditLog":{"properties":{"id":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Id"},"user_id":{"type":"integer","exclusiveMinimum":0.0,"title":"User Id"},"created":{"type":"string","format":"date-time","title":"Created","description":"When did this event occur","examples":["2026-02-17T00:46:02.709649Z"]},"level":{"$ref":"#/components/schemas/AuditLogLevel","description":"The level of importance for this event. Works the same as python logging levels. It is an integer 0 - 50, and implementers of this field could map it to the predefined levels: (`CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`).This is NOT the same concept as the 'strength' of whatever event happened; it is just for sorting, filtering and display purposes. For e.g. multiple level 20 events != the 'importance' of one level 40 event.","examples":[30]},"event_type":{"type":"string","maxLength":64,"title":"Event Type","examples":["entrance-limit"]},"event_msg":{"anyOf":[{"type":"string","maxLength":256,"minLength":3},{"type":"null"}],"title":"Event Msg","description":"The event message. Could be displayed on user's page"},"event_value":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Event Value","description":"Optionally store a numeric value associated with this event. For e.g. if we recalculate the user's normalized recon rate, and it is 'high', we could store an event like (event_type='recon-rate', event_msg='higher than allowed recon rate' event_value=0.42)","examples":[0.42]}},"type":"object","required":["user_id","level","event_type"],"title":"AuditLog","description":"Table / Model for logging \"actions\" taken by a user or \"events\" that\nare related to a User"},"AuditLogLevel":{"type":"integer","enum":[50,50,40,30,30,20,10,0],"title":"AuditLogLevel"},"Body_offerwall_softpair_block_post__product_id__offerwall_7a89dcdb__offerwall_id___post":{"properties":{"answers":{"items":{"$ref":"#/components/schemas/UserQuestionAnswerIn"},"type":"array","title":"Answers"}},"type":"object","required":["answers"],"title":"Body_offerwall_softpair_block_post__product_id__offerwall_7a89dcdb__offerwall_id___post"},"Body_offerwall_softpair_post__product_id__offerwall_37d1da64__offerwall_id___post":{"properties":{"answers":{"items":{"$ref":"#/components/schemas/UserQuestionAnswerIn"},"type":"array","title":"Answers"}},"type":"object","required":["answers"],"title":"Body_offerwall_softpair_post__product_id__offerwall_37d1da64__offerwall_id___post"},"Body_submit_profiling_questions__product_id__profiling_questions__post":{"properties":{"answers":{"items":{"$ref":"#/components/schemas/UserQuestionAnswerIn"},"type":"array","title":"Answers"}},"type":"object","required":["answers"],"title":"Body_submit_profiling_questions__product_id__profiling_questions__post"},"BrokerageProductPayoutEvent":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Payout Event Unique Identifier","examples":["9453cd076713426cb68d05591c7145aa"]},"debit_account_uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Debit Account Uuid","description":"The LedgerAccount.uuid that money is being requested from. Thie User or Brokerage Product is retrievable through the LedgerAccount.reference_uuid","examples":["18298cb1583846fbb06e4747b5310693"]},"cashout_method_uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Cashout Method Uuid","description":"References a row in the account_cashoutmethod table. This is the cashout method that was used to request this payout. (A cashout is the same thing as a payout)","examples":["a6dc1fc1bf934557b952f253dee12813"]},"created":{"type":"string","format":"date-time","title":"Created"},"amount":{"type":"integer","exclusiveMaximum":9.223372036854776e+18,"exclusiveMinimum":0.0,"title":"Amount","description":"The USDCent amount int. This cannot be 0 or negative","examples":[531]},"status":{"anyOf":[{"$ref":"#/components/schemas/PayoutStatus"},{"type":"null"}],"description":" - `PENDING` = PENDING\n - `APPROVED` = APPROVED\n - `REJECTED` = REJECTED\n - `CANCELLED` = CANCELLED\n - `FAILED` = FAILED\n - `COMPLETE` = COMPLETE","default":"PENDING","examples":["COMPLETE"]},"ext_ref_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ext Ref Id"},"payout_type":{"$ref":"#/components/schemas/PayoutType","description":" - `PAYPAL` = PAYPAL\n - `TANGO` = TANGO\n - `DWOLLA` = DWOLLA\n - `ACH` = ACH\n - `WIRE` = WIRE\n - `CASH_IN_MAIL` = CASH_IN_MAIL\n - `PRIZE` = PRIZE\n - `AMT` = AMT\n - `AMT_BONUS` = AMT_BONUS\n - `AMT_ASSIGNMENT` = AMT_HIT","examples":["ACH"]},"request_data":{"additionalProperties":true,"type":"object","title":"Request Data","description":"Stores payout-type-specific information that is used to request this payout from the external provider."},"order_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"$ref":"#/components/schemas/CashMailOrderData"},{"type":"null"}],"title":"Order Data","description":"Stores payout-type-specific order information that is returned from the external payout provider."},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","description":"The Brokerage Product that was paid out","examples":["1108d053e4fa47c5b0dbdcd03a7981e7"]},"method":{"$ref":"#/components/schemas/PayoutType","description":" - `PAYPAL` = PAYPAL\n - `TANGO` = TANGO\n - `DWOLLA` = DWOLLA\n - `ACH` = ACH\n - `WIRE` = WIRE\n - `CASH_IN_MAIL` = CASH_IN_MAIL\n - `PRIZE` = PRIZE\n - `AMT` = AMT\n - `AMT_BONUS` = AMT_BONUS\n - `AMT_ASSIGNMENT` = AMT_HIT","readOnly":true,"examples":["ACH"]},"amount_usd":{"type":"integer","minimum":0.0,"title":"Amount Usd","readOnly":true,"examples":["$10,000.000"]},"amount_usd_str":{"type":"string","title":"Amount Usd Str","readOnly":true,"examples":["$10,000.000"]}},"type":"object","required":["debit_account_uuid","cashout_method_uuid","amount","payout_type","product_id","method","amount_usd","amount_usd_str"],"title":"BrokerageProductPayoutEvent","description":"The amount\n\n- created: When the Brokerage Product was paid out"},"BucketTask":{"properties":{"id":{"type":"string","maxLength":32,"minLength":1,"title":"Id","description":"The internal task id for this task within the marketplace","examples":["6ov9jz3"]},"id_code":{"type":"string","maxLength":35,"minLength":3,"pattern":"^[a-z]{1,2}\\:.*","title":"Id Code","description":"The namespaced task id for this task within the marketplace","examples":["o:6ov9jz3"]},"source":{"$ref":"#/components/schemas/Source","examples":["o"]},"loi":{"type":"integer","maximum":5400.0,"exclusiveMinimum":1.0,"title":"Loi","description":"expected loi in seconds","examples":[612]},"payout":{"type":"integer","exclusiveMinimum":1.0,"title":"Payout","description":"integer cents","examples":[123]}},"type":"object","required":["id","id_code","source","loi","payout"],"title":"BucketTask","description":"This represents one of the \"tasks\" within a bucket's ordered list of tasks."},"Cardinality":{"type":"string","enum":["*","?"],"title":"Cardinality"},"CashMailCashoutMethodData":{"properties":{"type":{"type":"string","const":"CASH_IN_MAIL","title":"Type","default":"CASH_IN_MAIL"},"delivery_address":{"$ref":"#/components/schemas/USDeliveryAddress","description":"Delivery address where payment should be sent"}},"type":"object","required":["delivery_address"],"title":"CashMailCashoutMethodData"},"CashMailOrderData":{"properties":{"type":{"type":"string","const":"CASH_IN_MAIL","title":"Type","default":"CASH_IN_MAIL"},"shipping_cost":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Shipping Cost","description":"(USD cents) The shipping cost. This amount get charged to the BP."},"tracking_number":{"anyOf":[{"type":"string","maxLength":50,"minLength":1},{"type":"null"}],"title":"Tracking Number"},"shipping_method":{"anyOf":[{"$ref":"#/components/schemas/ShippingMethod","maxLength":50,"minLength":1},{"type":"null"}],"description":"Standard, express, etc."},"carrier":{"anyOf":[{"$ref":"#/components/schemas/ShippingCarrier","maxLength":50,"minLength":1},{"type":"null"}],"description":"Name of the shipping company, e.g., USPS, FedEx, DHL"},"ship_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ship Date"},"estimated_delivery_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Estimated Delivery Date"},"delivery_status":{"anyOf":[{"$ref":"#/components/schemas/DeliveryStatus","maxLength":50,"minLength":1},{"type":"null"}],"description":"Current status of delivery, e.g., pending, in transit, delivered"},"last_updated":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Updated","description":"Timestamp of the last status update"}},"type":"object","required":["shipping_cost"],"title":"CashMailOrderData"},"CashoutMethodForeignValue":{"properties":{"value":{"type":"integer","minimum":0.0,"title":"Value","description":"Value of the redemption in the currency's smallest unit."},"currency":{"$ref":"#/components/schemas/RedemptionCurrency","description":"Supported Currencies for Foreign Redemptions\n\nAllowed values: \n - __USD__ *(USD)*: US Dollars. Smallest Unit: Cents.\n - __CAD__ *(CAD)*: Canadian Dollars. Smallest Unit: Cents.\n - __GBP__ *(GBP)*: British Pounds. Smallest Unit: Pence.\n - __EUR__ *(EUR)*: Euros. Smallest Unit: Cents.\n - __INR__ *(INR)*: Indian Rupees. Smallest Unit: Paise.\n - __AUD__ *(AUD)*: Australian Dollars. Smallest Unit: Cents.\n - __PLN__ *(PLN)*: Polish Zloty. Smallest Unit: Grosz.\n - __SEK__ *(SEK)*: Swedish Krona. Smallest Unit: Öre.\n - __SGD__ *(SGD)*: Singapore Dollars. Smallest Unit: Cents.\n - __MXN__ *(MXN)*: Mexican Pesos. Smallest Unit: Centavos."},"value_string":{"type":"string","title":"Value String","description":"A string representation of the value in the currency."}},"type":"object","required":["value","currency","value_string"],"title":"CashoutMethodForeignValue","description":"Shows the expected value of a redemption in a foreign currency.","example":{"currency":"CAD","value":"138","value_string":"$1.38 CAD"}},"CashoutMethodForeignValueResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"cashout_method_value":{"$ref":"#/components/schemas/CashoutMethodForeignValue"}},"type":"object","required":["status","cashout_method_value"],"title":"CashoutMethodForeignValueResponse"},"CashoutMethodOut":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique ID for this cashout method"},"currency":{"type":"string","const":"USD","title":"Currency","description":"The currency of the cashout. Only USD is supported.","default":"USD"},"original_currency":{"anyOf":[{"$ref":"#/components/schemas/Currency"},{"type":"null"}],"description":"The base currency of the money paid out. This is used for e.g. sending an Amazon UK gift card"},"data":{"oneOf":[{"$ref":"#/components/schemas/PaypalCashoutMethodData"},{"$ref":"#/components/schemas/TangoCashoutMethodData"},{"$ref":"#/components/schemas/CashMailCashoutMethodData"},{"$ref":"#/components/schemas/AmtCashoutMethodData"}],"title":"Data","discriminator":{"propertyName":"type","mapping":{"AMT":"#/components/schemas/AmtCashoutMethodData","CASH_IN_MAIL":"#/components/schemas/CashMailCashoutMethodData","PAYPAL":"#/components/schemas/PaypalCashoutMethodData","TANGO":"#/components/schemas/TangoCashoutMethodData"}}},"description":{"type":"string","title":"Description","description":"The description of the cashout method.","default":""},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url","description":"Link to an image to display"},"max_value":{"type":"integer","exclusiveMinimum":0.0,"title":"Max Value","description":"(In lowest unit of the original_currency), The maximum amount that can be cashed out in one transaction."},"min_value":{"type":"integer","minimum":0.0,"title":"Min Value","description":"(In lowest unit of the original_currency), The minimum amount that can be cashed out in one transaction."},"name":{"type":"string","title":"Name","description":"A descriptive name for the cashout method."},"type":{"$ref":"#/components/schemas/PayoutType","description":"The method in which the requested payout is delivered.\n\nAllowed values: \n - __PAYPAL__ *(PAYPAL)*: User is paid out to their personal PayPal email address\n - __TANGO__ *(TANGO)*: User is paid uut via a Tango Gift Card\n - __DWOLLA__ *(DWOLLA)*: DWOLLA\n - __ACH__ *(ACH)*: A payment is made to a bank account using ACH\n - __WIRE__ *(WIRE)*: A payment is made to a bank account using ACH\n - __CASH_IN_MAIL__ *(CASH_IN_MAIL)*: A payment is made in cash and mailed to the user.\n - __PRIZE__ *(PRIZE)*: A payment is made as a prize with some monetary value\n - __AMT__ *(AMT)*: This is used to designate either AMT_BONUS or AMT_HIT\n - __AMT_BONUS__ *(AMT_BONUS)*: Amazon Mechanical Turk as a Bonus\n - __AMT_ASSIGNMENT__ *(AMT_HIT)*: Amazon Mechanical Turk for a HIT"},"ext_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ext Id","description":"An external ID. Can be shown to a user to disambiguate a user's possibly multiple methods"},"usd_exchange_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Usd Exchange Rate"},"max_value_usd":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Max Value Usd","description":"(In lowest unit of USD), The maximum amount that can be cashed out in one transaction."},"min_value_usd":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Min Value Usd","description":"(In lowest unit of USD), The minimum amount that can be cashed out in one transaction."},"product_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Product Id","examples":["4fe381fb7186416cb443a38fa66c6557"]},"product_user_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":3},{"type":"null"}],"title":"Product User Id","description":"A unique identifier for each user, which is set by the Supplier. It should not contain any sensitive informationlike email or names, and should avoid using anyincrementing values.","examples":["app-user-9329ebd"]}},"type":"object","required":["id","data","max_value","min_value","name","type"],"title":"CashoutMethodOut","example":{"bpid":"4a3ddfb747344bbc93efadf1c3a16e1a","currency":"USD","data":{"disclaimer":"...","terms":"..."},"description":"...","id":"941d489c3ce04eb39a0ddb7f8f75db74","image_url":"https://d30s7yzk2az89n.cloudfront.net/images/brands/b238587-1200w-326ppi.png","max_value":25000,"min_value":500,"name":"Visa® Prepaid Card USD","type":"TANGO"}},"CashoutMethodResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"cashout_method":{"$ref":"#/components/schemas/CashoutMethodOut"}},"type":"object","required":["status","cashout_method"],"title":"CashoutMethodResponse"},"CashoutMethodsResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"cashout_methods":{"items":{"$ref":"#/components/schemas/CashoutMethodOut"},"type":"array","title":"Cashout Methods"}},"type":"object","required":["status","cashout_methods"],"title":"CashoutMethodsResponse"},"CashoutRequestInfo":{"properties":{"id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Id","description":"Unique ID for this cashout. This may be NULL if the status is REJECTED or FAILED, which may happen if the request is invalid.","examples":["3ceb847aaf9f40f4bd15b2b5e083abf6"]},"description":{"type":"string","title":"Description","description":"This is the name of the cashout method.","examples":["Visa® Prepaid Card USD"]},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"status":{"anyOf":[{"$ref":"#/components/schemas/PayoutStatus"},{"type":"null"}],"description":" - `PENDING` = PENDING\n - `APPROVED` = APPROVED\n - `REJECTED` = REJECTED\n - `CANCELLED` = CANCELLED\n - `FAILED` = FAILED\n - `COMPLETE` = COMPLETE","default":"PENDING","examples":["PENDING"]},"transaction_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Transaction Info"}},"type":"object","required":["id","description"],"title":"CashoutRequestInfo","description":"See models.thl.payout: PayoutEvent. We've confused a CashOut and a\nPayout. This is used only in the API response."},"CashoutRequestResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"cashout":{"$ref":"#/components/schemas/CashoutRequestInfo"}},"type":"object","required":["status","cashout"],"title":"CashoutRequestResponse"},"Category":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid","examples":["30fad401c93c438cae1cffecb046fd3e"]},"adwords_vertical_id":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Adwords Vertical Id"},"label":{"type":"string","maxLength":255,"title":"Label","examples":["Hair Loss"]},"path":{"type":"string","pattern":"^\\/.*[^\\/]$","title":"Path","examples":["/Beauty & Fitness/Hair Care/Hair Loss"]},"parent_uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Parent Uuid","examples":["d52f3e2354ef434e9c89704fe1fb6018"]}},"type":"object","required":["uuid","label","path"],"title":"Category"},"CategoryAssociation":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"The category ID","examples":["c8642a1b86d9460cbe8f7e8ae6e56ee4"]},"label":{"type":"string","maxLength":255,"title":"Label","description":"The category label","examples":["People & Society"]},"adwords_id":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Adwords Id","examples":["14"]},"adwords_label":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Adwords Label","examples":["People & Society"]},"p":{"type":"number","maximum":1.0,"minimum":0.0,"title":"P","description":"The strength of the association of this bucketwith this category. Will sum to 1 within a bucket.","examples":[1.0]}},"type":"object","required":["id","label","p"],"title":"CategoryAssociation","description":"Used in an offerwall. Stores the association between a category\nand a bucket, with a score."},"ContestEndCondition":{"properties":{"target_entry_amount":{"anyOf":[{"anyOf":[{"type":"integer","minimum":0.0},{"type":"integer","exclusiveMinimum":0.0}],"ge":1},{"type":"null"}],"title":"Target Entry Amount","description":"The contest is over once this amount is reached. (sum of all entry amount)"},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At","description":"The contest is over at this time."}},"type":"object","title":"ContestEndCondition","description":"Defines the conditions to evaluate to determine when the contest is over.\nMultiple conditions can be set. The contest is over once ANY conditions are met."},"ContestEndReason":{"type":"string","enum":["cancelled","target_entry_amount","ends_at","max_winners"],"title":"ContestEndReason","description":"Defines why a contest ended"},"ContestEntryRule":{"properties":{"max_entry_amount_per_user":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Max Entry Amount Per User","description":"Maximum total value of entries per user"},"max_daily_entries_per_user":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Max Daily Entries Per User","description":"Maximum entries per user allowed per day for this contest"},"min_completes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Completes"},"min_membership_level":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Membership Level"},"id_verified":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Id Verified"}},"type":"object","title":"ContestEntryRule","description":"Defines rules the user must meet to be allowed to enter this contest\nOnly applies if the ContestType is ENTRY!"},"ContestEntryTrigger":{"type":"string","enum":["task_complete","task_attempt","referral"],"title":"ContestEntryTrigger","description":"Defines what action/event triggers a (possible) entry into the contest (automatically).\nThis only is valid on milestone contests"},"ContestPrize":{"properties":{"kind":{"$ref":"#/components/schemas/ContestPrizeKind","description":"\nAllowed values: \n - __physical__ *(PHYSICAL)*: A physical prize (e.g. a iPhone, cash in the mail, dinner with Max)\n - __promotion__ *(PROMOTION)*: A promotion is a temporary or special offer that provides extra value or benefits (e.g. 20% bonus on completes for the next 7 days)\n - __cash__ *(CASH)*: Money is deposited into user's virtual wallet"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"estimated_cash_value":{"type":"integer","minimum":0.0,"title":"Estimated Cash Value","description":"Estimated cash value of prize in USDCents"},"cash_amount":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Cash Amount","description":"If the kind=ContestPrizeKind.CASH, this is the amount of the prize"},"promotion_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Promotion Id","description":"If the kind=ContestPrizeKind.PROMOTION, this is the promotion ID"},"leaderboard_rank":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Leaderboard Rank","description":"The prize is for achieving this rank in the associated leaderboard. The highest rank is 1."}},"type":"object","required":["kind","estimated_cash_value"],"title":"ContestPrize"},"ContestPrizeKind":{"type":"string","enum":["physical","promotion","cash"],"title":"ContestPrizeKind"},"ContestStatus":{"type":"string","enum":["active","completed","cancelled"],"title":"ContestStatus"},"ContestWinner":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When this user won this prize"},"prize":{"$ref":"#/components/schemas/ContestPrize"},"awarded_cash_amount":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Awarded Cash Amount","description":"The actual amount this user receives. For cash prizes, if there was a tie, this could be different from the prize amount."},"product_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Product User Id","readOnly":true}},"type":"object","required":["prize","product_user_id"],"title":"ContestWinner","description":"In a Raffle, the ContestEntryType can be COUNT or CASH. In the CASH type, the unit\nof entry is 1 USDCent (one penny). Implicitly, each penny entered buys 1 entry\ninto the raffle, and one entry is randomly selected for each prize.\n\nA contest should have as many winners as there are prizes\nspecial case 1: there are fewer entries than prizes\nspecial case 2: leaderboard contest with ties"},"CountStat":{"properties":{"facet":{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"}]},"propertyNames":{"enum":["country_iso","day","month"]},"type":"object","title":"Facet","description":"The grouping criteria","examples":[{"country_iso":"us"}]},"count":{"type":"integer","title":"Count","description":"The count value for the given metric and facet"}},"type":"object","required":["facet","count"],"title":"CountStat"},"CreateCashMailCashoutMethodRequest":{"properties":{"bpuid":{"type":"string","maxLength":128,"minLength":3,"title":"Bpuid","description":"(product_user_id) The user to create this cashout method for.","examples":["app-user-9329ebd"]},"type":{"type":"string","const":"CASH_IN_MAIL","title":"Type","default":"CASH_IN_MAIL"},"delivery_address":{"$ref":"#/components/schemas/USDeliveryAddress","description":"Delivery address where payment should be sent"}},"type":"object","required":["bpuid","delivery_address"],"title":"CreateCashMailCashoutMethodRequest"},"CreateCashoutRequest":{"properties":{"bpuid":{"type":"string","maxLength":128,"minLength":3,"title":"Bpuid","description":"(product_user_id) The user requesting a cashout.","examples":["app-user-9329ebd"]},"amount":{"type":"integer","exclusiveMinimum":0.0,"title":"Amount","description":"(USD cents) The amount requested for the cashout.","examples":[531]},"cashout_method_id":{"type":"string","maxLength":32,"minLength":32,"title":"Cashout Method Id","description":"Unique ID for the cashout method the cashout is being requested with.","examples":["941d489c3ce04eb39a0ddb7f8f75db74"]}},"type":"object","required":["bpuid","amount","cashout_method_id"],"title":"CreateCashoutRequest"},"CreatePayPalCashoutMethodRequest":{"properties":{"bpuid":{"type":"string","maxLength":128,"minLength":3,"title":"Bpuid","description":"(product_user_id) The user to create this cashout method for.","examples":["app-user-9329ebd"]},"type":{"type":"string","const":"PAYPAL","title":"Type","default":"PAYPAL"},"email":{"type":"string","format":"email","title":"Email","description":"Email address of the paypal user","examples":["test@example.com"]}},"type":"object","required":["bpuid","email"],"title":"CreatePayPalCashoutMethodRequest"},"Currency":{"type":"string","enum":["USD","CAD","GBP","EUR","INR","AUD","PLN","SEK","SGD","MXN"],"title":"Currency"},"DeliveryStatus":{"type":"string","enum":["Pending","Shipped","In Transit","Out for Delivery","Delivered","Returned","Canceled","Failed Attempt","Lost"],"title":"DeliveryStatus"},"Direction":{"type":"integer","enum":[-1,1],"title":"Direction","description":"Entries on the debit side will increase debit normal accounts, while\nentries on the credit side will decrease them. Conversely, entries on\nthe credit side will increase credit normal accounts, while entries on\nthe debit side will decrease them.\n\nBy convention (?), the db will store transactions as debit-normal. For\na credit-normal account, we should flip the signs."},"DurationSummary":{"properties":{"min":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Min"},"max":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Max"},"mean":{"anyOf":[{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Mean"},"q1":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Q1"},"q2":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Q2"},"q3":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Q3"},"lower_whisker":{"type":"integer","title":"Lower Whisker","readOnly":true},"upper_whisker":{"type":"integer","title":"Upper Whisker","readOnly":true}},"type":"object","required":["min","max","q1","q2","q3","lower_whisker","upper_whisker"],"title":"DurationSummary","description":"Durations are in integer seconds.\nDescribes the statistical distribution of expected durations of tasks within this bucket.","examples":[{"max":1180,"mean":660,"min":112,"q1":457,"q2":650,"q3":1103}]},"EventEnvelope":{"properties":{"event_uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Event Uuid"},"event_type":{"$ref":"#/components/schemas/EventType"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"},"version":{"type":"integer","title":"Version","default":1},"product_user_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":3},{"type":"null"}],"title":"Product User Id","description":"A unique identifier for each user. This is hidden unlessthe event is for the requesting user.","examples":["app-user-9329ebd"]},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","examples":["4fe381fb7186416cb443a38fa66c6557"]},"payload":{"oneOf":[{"$ref":"#/components/schemas/TaskEnterPayload"},{"$ref":"#/components/schemas/TaskFinishPayload"},{"$ref":"#/components/schemas/SessionEnterPayload"},{"$ref":"#/components/schemas/SessionFinishPayload"}],"title":"Payload","discriminator":{"propertyName":"event_type","mapping":{"session.enter":"#/components/schemas/SessionEnterPayload","session.finish":"#/components/schemas/SessionFinishPayload","task.enter":"#/components/schemas/TaskEnterPayload","task.finish":"#/components/schemas/TaskFinishPayload"}}}},"type":"object","required":["event_type","product_id","payload"],"title":"EventEnvelope"},"EventMessage":{"properties":{"kind":{"type":"string","const":"event","title":"Kind","default":"event"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"},"data":{"$ref":"#/components/schemas/EventEnvelope"}},"type":"object","required":["data"],"title":"EventMessage"},"EventType":{"type":"string","enum":["task.enter","task.finish","session.enter","session.finish","wallet.credit","wallet.debit","user.created","user.active"],"title":"EventType"},"GeoIPInformation":{"properties":{"geoname_id":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Geoname Id"},"continent_code":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Continent Code"},"continent_name":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Continent Name"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso","description":"The ISO code of the country associated with the IP address.","examples":["jm"]},"country_name":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Country Name"},"subdivision_1_iso":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Subdivision 1 Iso","description":"The ISO code of the primary subdivision (e.g., state or province)."},"subdivision_1_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Subdivision 1 Name","description":"The name of the primary subdivision (e.g., state or province)."},"subdivision_2_iso":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Subdivision 2 Iso","description":"The ISO code of the secondary subdivision (if applicable)."},"subdivision_2_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Subdivision 2 Name","description":"The name of the secondary subdivision (if applicable)."},"city_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"City Name","description":"The name of the city associated with the IP address.","examples":["Jamestown"]},"metro_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Metro Code"},"time_zone":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Time Zone","description":"The time zone associated with the geographical location.","examples":["Europe/Madrid"]},"is_in_european_union":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is In European Union"},"updated":{"type":"string","format":"date-time","title":"Updated"},"ip":{"type":"string","title":"Ip"},"registered_country_iso":{"anyOf":[{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},{"type":"null"}],"title":"Registered Country Iso","description":"The ISO code of the country where the IP address is registered.","examples":["sm"]},"is_anonymous":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Anonymous","description":"Indicates whether the IP address is associated with an anonymous source (e.g., VPN, proxy).","examples":[false]},"is_anonymous_vpn":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Anonymous Vpn"},"is_hosting_provider":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Hosting Provider"},"is_public_proxy":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public Proxy"},"is_tor_exit_node":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Tor Exit Node"},"is_residential_proxy":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Residential Proxy"},"autonomous_system_number":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Autonomous System Number"},"autonomous_system_organization":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Autonomous System Organization"},"domain":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Domain"},"isp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Isp","description":"The Internet Service Provider associated with the IP address.","examples":["Comcast"]},"mobile_country_code":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Mobile Country Code"},"mobile_network_code":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Mobile Network Code"},"network":{"anyOf":[{"type":"string","maxLength":56},{"type":"null"}],"title":"Network"},"organization":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Organization"},"static_ip_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Static Ip Score","description":"A score indicating the likelihood that the IP address is static."},"user_type":{"anyOf":[{"$ref":"#/components/schemas/UserType"},{"type":"null"}],"description":"The type of user associated with the IP address (e.g., 'residential', 'business').","examples":["school"]},"postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postal Code","description":"The postal code associated with the IP address.","examples":["13245"]},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latitude","description":"The latitude coordinate of the IP address location.","examples":[10.031844]},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Longitude","description":"The longitude coordinate of the IP address location.","examples":[-20.111975]},"accuracy_radius":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Accuracy Radius","description":"The approximate radius of accuracy for the latitude and longitude, in kilometers.","examples":[167]}},"type":"object","required":["country_iso","ip"],"title":"GeoIPInformation"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"IntegrationMode":{"type":"string","enum":["platform","pass_through"],"title":"IntegrationMode"},"Leaderboard":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique ID for this leaderboard","examples":["845b0074ad533df580ebb9c80cc3bce1"]},"name":{"type":"string","title":"Name","description":"Descriptive name for the leaderboard based on the board_code","examples":["Number of Completes"]},"board_code":{"$ref":"#/components/schemas/LeaderboardCode","description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts","examples":["complete_count"]},"bpid":{"type":"string","maxLength":32,"minLength":32,"title":"Bpid","description":"product_id","examples":["4fe381fb7186416cb443a38fa66c6557"]},"country_iso":{"type":"string","title":"Country Iso","description":"The country this leaderboard is for.","examples":["us"]},"freq":{"$ref":"#/components/schemas/LeaderboardFrequency","description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00","examples":["daily"]},"timezone_name":{"type":"string","title":"Timezone Name","description":"The timezone for the requested country","examples":["America/New_York"]},"sort_order":{"type":"string","enum":["ascending","descending"],"title":"Sort Order","default":"descending"},"row_count":{"type":"integer","minimum":0.0,"title":"Row Count","description":"The total number of rows in the leaderboard.","examples":[2]},"rows":{"items":{"$ref":"#/components/schemas/LeaderboardRow"},"type":"array","title":"Rows","examples":[[{"bpuid":"app-user-9329ebd","rank":1,"value":4},{"bpuid":"app-user-7923skw","rank":2,"value":3}]]},"period_start_local":{"type":"string","format":"date-time","title":"Period Start Local","description":"The start of the time period covered by this board in local time, tz-aware","examples":["2024-07-12T00:00:00-04:00"]},"period_end_local":{"type":"string","format":"date-time","title":"Period End Local","description":"The end of the time period covered by this board in local time, tz-aware","examples":["2024-07-12T23:59:59.999999-04:00"]},"start_timestamp":{"type":"integer","title":"Start Timestamp","description":"(unix timestamp) The start time of the time range this leaderboard covers.","readOnly":true,"examples":[1720756800]},"end_timestamp":{"type":"integer","title":"End Timestamp","description":"(unix timestamp) The end time of the time range this leaderboard covers.","readOnly":true,"examples":[1720843199]},"utc_offset":{"type":"string","title":"Utc Offset","description":"The UTC offset for the timezone","readOnly":true,"examples":["-0400"]},"local_start_time":{"type":"string","title":"Local Start Time","description":"The start time of the time range this leaderboard covers (local time, in the leaderboard's timezone).","readOnly":true,"examples":["2024-07-12T00:00:00-04:00"]},"local_end_time":{"type":"string","title":"Local End Time","description":"The end time of the time range this leaderboard covers (local time, in the leaderboard's timezone).","readOnly":true,"examples":["2024-07-12T23:59:59.999999-04:00"]},"start_end_str":{"type":"string","title":"Start End Str","description":"A formatted string for time period covered by this leaderboard. Can be used to display to users.","readOnly":true,"examples":["2024-02-07 to 2024-02-08"]}},"type":"object","required":["board_code","bpid","country_iso","freq","row_count","period_start_local","start_timestamp","end_timestamp","utc_offset","local_start_time","local_end_time","start_end_str"],"title":"Leaderboard","description":"A leaderboard exists independently for each product_id in each country.\nEach country is associated with a single timezone. There is a daily,\nweekly, and monthly leaderboard."},"LeaderboardCode":{"type":"string","enum":["complete_count","largest_user_payout","sum_user_payout"],"title":"LeaderboardCode","description":"The type of leaderboard. What the \"values\" represent."},"LeaderboardContestUserView":{"properties":{"name":{"type":"string","maxLength":128,"title":"Name","description":"Name of contest. Can be displayed to user."},"description":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Description","description":"Description of contest. Can be displayed to user."},"contest_type":{"type":"string","const":"leaderboard","title":"Contest Type","default":"leaderboard"},"end_condition":{"$ref":"#/components/schemas/ContestEndCondition"},"prizes":{"items":{"$ref":"#/components/schemas/ContestPrize"},"type":"array","minItems":1,"title":"Prizes"},"starts_at":{"type":"string","format":"date-time","title":"Starts At","description":"When the contest starts"},"terms_and_conditions":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Terms And Conditions"},"status":{"$ref":"#/components/schemas/ContestStatus","default":"active"},"country_isos":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Isos","description":"Contest is restricted to these countries. If null, all countries are allowed"},"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","description":"Contest applies only to a single BP"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When this contest was created"},"updated_at":{"type":"string","format":"date-time","title":"Updated At","description":"When this contest was last modified. Does not include entries being created/modified"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At","description":"When the contest ended"},"end_reason":{"anyOf":[{"$ref":"#/components/schemas/ContestEndReason"},{"type":"null"}],"description":"The reason the contest ended"},"product_user_id":{"type":"string","title":"Product User Id"},"user_winnings":{"items":{"$ref":"#/components/schemas/ContestWinner"},"type":"array","title":"User Winnings","description":"The prizes won in this contest by the requested user"},"leaderboard_key":{"type":"string","title":"Leaderboard Key","description":"The specific leaderboard instance this contest is connected to","examples":["leaderboard:7a9d8d02334449ceb105764f77e1ba97:us:weekly:2025-05-26:complete_count"]},"tie_break_strategy":{"type":"string","const":"split_prize_pool","title":"Tie Break Strategy","default":"split_prize_pool"},"country_iso":{"type":"string","title":"Country Iso","readOnly":true},"user_rank":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Rank","description":"The current rank of this user in this contest","readOnly":true}},"additionalProperties":false,"type":"object","required":["name","product_id","product_user_id","leaderboard_key","country_iso","user_rank"],"title":"LeaderboardContestUserView","example":{"contest_type":"leaderboard","country_iso":"us","country_isos":"us","created_at":"2026-02-18T07:03:18.635186Z","description":"$15 1st place, $10 2nd, $5 3rd place US weekly","end_condition":{"ends_at":"2025-06-02T05:29:59.999999Z"},"leaderboard_key":"leaderboard:1108d053e4fa47c5b0dbdcd03a7981e7:us:weekly:2025-05-26:complete_count","name":"Prizes for top survey takers this week","prizes":[{"cash_amount":1500,"estimated_cash_value":1500,"kind":"cash","leaderboard_rank":1,"name":"$15 Cash"},{"cash_amount":1000,"estimated_cash_value":1000,"kind":"cash","leaderboard_rank":2,"name":"$10 Cash"},{"cash_amount":500,"estimated_cash_value":500,"kind":"cash","leaderboard_rank":3,"name":"$5 Cash"}],"product_id":"1108d053e4fa47c5b0dbdcd03a7981e7","product_user_id":"test-user","starts_at":"2026-02-18T07:03:18.635162Z","status":"active","tie_break_strategy":"split_prize_pool","updated_at":"2026-02-18T07:03:18.635187Z","user_winnings":[],"uuid":"6adfa26d2b044cfa8bf0622041ee7c96"}},"LeaderboardFrequency":{"type":"string","enum":["daily","weekly","monthly"],"title":"LeaderboardFrequency","description":"The time period range for the leaderboard."},"LeaderboardResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"leaderboard":{"$ref":"#/components/schemas/Leaderboard"}},"type":"object","required":["status","leaderboard"],"title":"LeaderboardResponse"},"LeaderboardRow":{"properties":{"bpuid":{"type":"string","title":"Bpuid","description":"product_user_id","examples":["app-user-9329ebd"]},"rank":{"type":"integer","title":"Rank","description":"The numerical data ranks (1 through n) of the values. Ties are ranked using the lowest rank in the group.","examples":[1]},"value":{"type":"integer","title":"Value","description":"The value. The meaning of the value is dependent on the LeaderboardCode.","examples":[7]}},"type":"object","required":["bpuid","rank","value"],"title":"LeaderboardRow"},"LeaderboardWinner":{"properties":{"rank":{"type":"integer","title":"Rank","description":"The user's final rank in the leaderboard","examples":[1]},"freq":{"$ref":"#/components/schemas/LeaderboardFrequency","description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00","examples":["daily"]},"board_code":{"$ref":"#/components/schemas/LeaderboardCode","description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts","examples":["complete_count"]},"country_iso":{"type":"string","title":"Country Iso","description":"The country this leaderboard is for.","examples":["us"]},"issued":{"type":"string","format":"date-time","title":"Issued","description":"When the prize was issued.","examples":["2022-10-17T05:59:14.570231Z"]},"bpuid":{"type":"string","title":"Bpuid","description":"product_user_id","examples":["app-user-9329ebd"]},"description":{"type":"string","title":"Description","examples":["Bonus for daily contest"]},"amount":{"type":"integer","title":"Amount","description":"(USD cents) The reward amount","examples":[1000]},"amount_str":{"type":"string","title":"Amount Str","description":"The amount as a formatted string in USD. Can be displayed to the user.","examples":["$10.00"]},"contest_start":{"type":"string","format":"date-time","title":"Contest Start","description":"When the leaderboard started","examples":["2022-10-16T04:00:00Z"]},"contest_end":{"type":"string","format":"date-time","title":"Contest End","description":"When the leaderboard ended","examples":["2022-10-17T04:00:00Z"]}},"type":"object","required":["rank","freq","board_code","country_iso","issued","bpuid","description","amount","amount_str","contest_start","contest_end"],"title":"LeaderboardWinner"},"LeaderboardWinnerResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"winners":{"items":{"$ref":"#/components/schemas/LeaderboardWinner"},"type":"array","title":"Winners"}},"type":"object","required":["status"],"title":"LeaderboardWinnerResponse"},"LedgerAccount":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid","description":"A unique identifier for this Ledger Account","examples":["c3c3566b5b1b4961b63a5670a2dc923d"]},"display_name":{"type":"string","maxLength":64,"title":"Display Name","description":"Human-readable description of the Ledger Account","examples":["BP Wallet c3c3566b5b1b4961b63a5670a2dc923d"]},"qualified_name":{"type":"string","maxLength":255,"title":"Qualified Name"},"account_type":{"$ref":"#/components/schemas/AccountType","description":" - `bp_commission` = BP_COMMISSION\n - `bp_wallet` = BP_WALLET\n - `user_wallet` = USER_WALLET\n - `cash` = CASH\n - `revenue` = REVENUE\n - `expense` = EXPENSE\n - `contest_wallet` = CONTEST_WALLET\n - `credit_line` = CREDIT_LINE\n - `wa_wallet` = WA_WALLET\n - `wa_budget_pool` = WA_BUDGET_POOL\n - `wa_held` = WA_HELD\n - `wa_credit_line` = WA_CREDIT_LINE","examples":["bp_wallet"]},"normal_balance":{"$ref":"#/components/schemas/Direction","description":" - `-1` = CREDIT\n - `1` = DEBIT"},"reference_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference Type"},"reference_uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Reference Uuid","description":"The associated Product ID or other parent account thatthis Ledger Account is intended to track transactions for.If Wallet mode is enabled, this can also handle trackingindividual users.","examples":["61dd0b086fd048518762757612b4a6d3"]},"currency":{"type":"string","maxLength":32,"title":"Currency","description":"GRL's Ledger system allows tracking of transactions inany currency possible. This is useful for trackingpoints, stars, coins, or any other currency that may beused in a Supplier's platform.","default":"USD"}},"type":"object","required":["display_name","qualified_name","account_type","normal_balance"],"title":"LedgerAccount"},"LedgerEntry":{"properties":{"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"direction":{"$ref":"#/components/schemas/Direction"},"account_uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Account Uuid"},"amount":{"type":"integer","exclusiveMaximum":9.223372036854776e+18,"exclusiveMinimum":0.0,"title":"Amount","description":"The USDCent amount. A LedgerEntry cannot be made for0 USDCent and it cannot be negative.","examples":[531]},"transaction_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Transaction Id"}},"type":"object","required":["direction","account_uuid","amount"],"title":"LedgerEntry"},"LedgerTransaction":{"properties":{"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"created":{"type":"string","format":"date-time","title":"Created","description":"When the Transaction (TX) was created into the database.This does not represent the exact time for any actionwhich may be responsible for this Transaction (TX), and TX timestamps will likely be a few milliseconds delayed"},"ext_description":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Ext Description"},"tag":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Tag"},"metadata":{"additionalProperties":{"type":"string"},"type":"object","title":"Metadata"},"entries":{"items":{"$ref":"#/components/schemas/LedgerEntry"},"type":"array","title":"Entries","description":"A Transaction (TX) is composed of multiple Entry events."}},"additionalProperties":false,"type":"object","title":"LedgerTransaction"},"MarketProfileKnowledge":{"properties":{"source":{"$ref":"#/components/schemas/Source","maxLength":16,"description":"Marketplace this question is from"},"question_id":{"type":"string","title":"Question Id","examples":["gender","1843","gender_plus"]},"answer":{"items":{"type":"string"},"type":"array","title":"Answer","examples":[["male"],["7657644"],["1"]]},"created":{"type":"string","format":"date-time","title":"Created","description":"When the User submitted this Profiling data"}},"type":"object","required":["source","question_id","created"],"title":"MarketProfileKnowledge","description":"This is used solely in API responses, so it is simplified."},"MarketplaceBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"x":{"type":"integer","minimum":0.0,"title":"X","description":"For UI. Provides a dimensionality position for the bucket on the x-axis.","default":0,"examples":[0,1,2]},"y":{"type":"integer","minimum":0.0,"title":"Y","description":"For UI. Provides a dimensionality position for the bucket on the y-axis.","default":0,"examples":[0,1,2]},"name":{"type":"string","title":"Name","description":"Currently unused. Will always return empty string","default":""},"description":{"type":"string","title":"Description","description":"Currently unused. Will always return empty string","default":""},"category":{"items":{"$ref":"#/components/schemas/CategoryAssociation"},"type":"array","title":"Category"},"contents":{"items":{"$ref":"#/components/schemas/BucketTask"},"type":"array","title":"Contents"},"duration":{"$ref":"#/components/schemas/DurationSummary"},"payout":{"$ref":"#/components/schemas/PayoutSummary"},"source":{"type":"string","enum":["innovate","dynata","schlesinger","purespectrum","morning","pollfish","precision","repdata","prodege"],"title":"Source","description":"this is the source of the contents","examples":["pollfish"]}},"additionalProperties":false,"type":"object","required":["id","uri","contents","duration","payout","source"],"title":"MarketplaceBucket"},"MarketplaceInventorySummary":{"properties":{"live_tasks":{"items":{"$ref":"#/components/schemas/CountStat"},"type":"array","title":"Live Tasks","description":"The count of tasks that are currently live"},"live_gen_pop_tasks":{"items":{"$ref":"#/components/schemas/CountStat"},"type":"array","title":"Live Gen Pop Tasks","description":"The count of gen-pop tasks that are currently live"},"tasks_created":{"items":{"$ref":"#/components/schemas/CountStat"},"type":"array","title":"Tasks Created","description":"The count of tasks created"},"required_finishes":{"items":{"$ref":"#/components/schemas/CountStat"},"type":"array","title":"Required Finishes","description":"Number of finishes needed across all live tasks"},"payout":{"items":{"$ref":"#/components/schemas/StatisticalSummaryStat"},"type":"array","title":"Payout","description":"The distribution of payouts for all live tasks"},"expected_duration":{"items":{"$ref":"#/components/schemas/StatisticalSummaryStat"},"type":"array","title":"Expected Duration","description":"The distribution of expected durations for all live tasks"},"required_finishes_per_task":{"items":{"$ref":"#/components/schemas/StatisticalSummaryStat"},"type":"array","title":"Required Finishes Per Task","description":"The distribution of required finishes on all live tasks"}},"type":"object","title":"MarketplaceInventorySummary"},"MarketplaceOfferwall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/MarketplaceBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"MarketplaceOfferwall","description":"Returns buckets grouped by marketplace, one per marketplace, with the\ntasks ordered by quality.\n\nOfferwall code: `5fa23085`"},"MarketplaceOfferwallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/MarketplaceOfferwall"}},"type":"object","required":["info","offerwall"],"title":"MarketplaceOfferwallResponse"},"MarketplaceSummary":{"properties":{"inventory":{"$ref":"#/components/schemas/MarketplaceInventorySummary","description":"Inventory of the marketplace"},"user_activity":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Activity","description":"User activity of the marketplace"}},"type":"object","required":["inventory"],"title":"MarketplaceSummary"},"MaxGaugeBySource":{"properties":{"value":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Value"},"by_source":{"additionalProperties":{"type":"integer","minimum":0.0},"propertyNames":{"$ref":"#/components/schemas/Source"},"type":"object","title":"By Source"}},"type":"object","title":"MaxGaugeBySource"},"MilestoneContestEndCondition":{"properties":{"max_winners":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Max Winners","description":"The contest will end once this many users have won (i.e. reachedthe milestone)."},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At","description":"The Contest is over at the ends_at time."}},"type":"object","title":"MilestoneContestEndCondition","description":"Defines the conditions to evaluate to determine when the contest is over.\nMultiple conditions can be set. The contest is over once ANY conditions are met."},"MilestoneUserView":{"properties":{"target_amount":{"type":"integer","exclusiveMinimum":0.0,"title":"Target Amount","description":"Each user 'wins' (receives prizes) once this target amount is reached."},"entry_trigger":{"anyOf":[{"$ref":"#/components/schemas/ContestEntryTrigger"},{"type":"null"}],"description":"What user action triggers an entry automatically."},"valid_for":{"anyOf":[{"type":"string","format":"duration"},{"type":"null"}],"title":"Valid For","description":"The time after valid_for_event for which the contest is open"},"valid_for_event":{"anyOf":[{"type":"string","const":"signup"},{"type":"null"}],"title":"Valid For Event"},"name":{"type":"string","maxLength":128,"title":"Name","description":"Name of contest. Can be displayed to user."},"description":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Description","description":"Description of contest. Can be displayed to user."},"contest_type":{"type":"string","const":"milestone","title":"Contest Type","default":"milestone"},"end_condition":{"$ref":"#/components/schemas/MilestoneContestEndCondition"},"prizes":{"items":{"$ref":"#/components/schemas/ContestPrize"},"type":"array","minItems":1,"title":"Prizes"},"starts_at":{"type":"string","format":"date-time","title":"Starts At","description":"When the contest starts"},"terms_and_conditions":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Terms And Conditions"},"status":{"$ref":"#/components/schemas/ContestStatus","default":"active"},"country_isos":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Isos","description":"Contest is restricted to these countries. If null, all countries are allowed"},"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","description":"Contest applies only to a single BP"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When this contest was created"},"updated_at":{"type":"string","format":"date-time","title":"Updated At","description":"When this contest was last modified. Does not include entries being created/modified"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At","description":"When the contest ended"},"end_reason":{"anyOf":[{"$ref":"#/components/schemas/ContestEndReason"},{"type":"null"}],"description":"The reason the contest ended"},"product_user_id":{"type":"string","title":"Product User Id"},"user_winnings":{"items":{"$ref":"#/components/schemas/ContestWinner"},"type":"array","title":"User Winnings","description":"The prizes won in this contest by the requested user"},"entry_type":{"type":"string","const":"count","title":"Entry Type","default":"count"},"win_count":{"type":"integer","title":"Win Count","description":"The number of times the milestone has been reached.","default":0},"user_amount":{"type":"integer","title":"User Amount","description":"The total amount for this user for this contest"}},"additionalProperties":false,"type":"object","required":["target_amount","name","end_condition","product_id","product_user_id","user_amount"],"title":"MilestoneUserView","example":{"contest_type":"milestone","created_at":"2025-06-12T21:12:58.061205Z","description":"Only valid for the first 50 users","end_condition":{"max_winners":50},"entry_trigger":"task_complete","entry_type":"count","name":"Win a 50% bonus for 7 days and a $5 bonus after your first 10 completes!","prizes":[{"estimated_cash_value":0,"kind":"promotion","name":"50% bonus on completes for 7 days"},{"cash_amount":500,"estimated_cash_value":500,"kind":"cash","name":"$5.00 Bonus"}],"product_id":"1108d053e4fa47c5b0dbdcd03a7981e7","product_user_id":"test-user","starts_at":"2025-06-12T21:12:58.061170Z","status":"active","target_amount":10,"terms_and_conditions":"https://www.example.com/","updated_at":"2025-06-12T21:12:58.061205Z","user_amount":8,"user_winnings":[],"uuid":"747fe3b709ae460e816821dcb81aebb9","win_count":12}},"OfferWallCategoryRequest":{"properties":{"adwords_category":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Adwords Category","examples":[["45","65"]]},"category":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Category","examples":[["98c137e4e90a4d92ac6c00e523eb1b50"]]},"exclude_adwords_category":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Exclude Adwords Category","examples":[["1558"]]},"exclude_category":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Exclude Category","examples":[["21536f160f784189be6194ca894f3a65","7aa8bf4e71a84dc3b2035f93f9f9c77e"]]}},"type":"object","title":"OfferWallCategoryRequest"},"OfferWallInfo":{"properties":{"success":{"type":"boolean","title":"Success"}},"type":"object","required":["success"],"title":"OfferWallInfo"},"OfferWallRequestYieldmanParams":{"properties":{"conversion_factor_adj":{"type":"number","title":"Conversion Factor Adj","description":"Centered around 0. Higher results in higher weight given to conversion (in the scoring function)","default":0.0},"dropoffrate_factor_adj":{"type":"number","title":"Dropoffrate Factor Adj","description":"Centered around 0. Higher results in higher penalty given to dropoffs (in the scoring function)","default":0.0},"longfail_factor_adj":{"type":"number","title":"Longfail Factor Adj","description":"Centered around 0. Higher results in higher penalty given to long fail (in the scoring function)","default":0.0},"recon_factor_adj":{"type":"number","title":"Recon Factor Adj","description":"Centered around 0. Higher results in higher penalty given to recons (in the scoring function)","default":0.0},"recon_likelihood_max":{"type":"number","title":"Recon Likelihood Max","description":"Tolerance for recon likelihood (0 to 1)","default":0.8}},"type":"object","title":"OfferWallRequestYieldmanParams"},"OfferwallConfig":{"properties":{},"type":"object","title":"OfferwallConfig"},"OfferwallReason":{"type":"string","enum":["USER_BLOCKED","HIGH_RECON_RATE","UNCOMMON_DEMOGRAPHICS","UNDER_MINIMUM_AGE","EXHAUSTED_HIGH_VALUE_SUPPLY","ALL_ELIGIBLE_ATTEMPTED","LOW_CURRENT_SUPPLY"],"title":"OfferwallReason"},"OneShotOfferwall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/OneShotOfferwallBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"OneShotOfferwall","description":"Each bucket has only 1 single task, and only basic info is returned\n about each bucket.\n\nOfferwall code: `6f27b1ae`"},"OneShotOfferwallBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"duration":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Duration","description":"The bucket's expected duration, in seconds"},"min_payout":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Min Payout","description":"The bucket's min payout, in usd cents"}},"additionalProperties":false,"type":"object","required":["id","uri","duration","min_payout"],"title":"OneShotOfferwallBucket"},"OneShotOfferwallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/OneShotOfferwall"}},"type":"object","required":["info","offerwall"],"title":"OneShotOfferwallResponse"},"POPFinancial":{"properties":{"product_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Product Id","examples":["3d350f18fdfe41f5a0e95b5e1c28733e"]},"time":{"type":"string","format":"date-time","title":"Time","description":"The starting time block for the respective 'Period' thatthis grouping is on. The `time` could be the start of a 1 minute or 1 hour block for example."},"payout":{"type":"integer","minimum":0.0,"title":"Payout","description":"The total amount (in USD cents) that the Brokerage Producthas earned within a respective time period.","default":0,"examples":[8359]},"adjustment":{"type":"integer","title":"Adjustment","description":"The total amount (in USD cents) that the Brokerage Producthas had adjusted within a respective time period. Most ofthe time, this will be negative due to Complete to Incomplete reconciliations. However, it can also be positive due to Incomplete to Complete adjustments.","examples":[1911]},"adjustment_types":{"items":{"$ref":"#/components/schemas/AdjustmentType"},"type":"array","title":"Adjustment Types"},"expense":{"type":"integer","title":"Expense","description":"For Product accounts that are setup with Respondent payouts,competitions, user bonuses, or other associated 'costs', thoseexpenses are accounted for here. This will be negative forthose types of costs."},"net":{"type":"integer","title":"Net","description":"This is the sum of the Payout total, Adjustment and any Expenses total. It can be positive or negative for any specific time period.","examples":[10270]},"payment":{"type":"integer","title":"Payment","description":"Any ACH or Wire amount that was issued between GRL and the Supplier.","examples":[3408288]}},"type":"object","required":["time","adjustment","adjustment_types","expense","net","payment"],"title":"POPFinancial","description":"We can't use our USDCent class in here because aside from it not\n supporting negative values for our adjustments, FastAPI also\n complains because it doesn't know how to generate documentation\n for it. - Max 2024-06-25"},"PatternValidation":{"properties":{"message":{"type":"string","title":"Message","description":"Message to display if validation fails"},"pattern":{"type":"string","title":"Pattern","description":"Regex string to validate. min_length and max_length are checked separately, even if they are part of the regex."}},"type":"object","required":["message","pattern"],"title":"PatternValidation"},"PayoutConfig":{"properties":{"payout_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payout Format","description":"\nThe format describing the str representation of a payout. Typically, this would be displayed to a user.\nThe payout_format is similar to python format string with a subset of functionality supported.\nOnly float with a precision are supported along with an optional comma for a thousands separator.\nIn addition, a mathematical operator can be applied, such as dividing by 100. \nExamples are shown assuming payout = 100 (one dollar).\n- \"{payout*10:,.0f} Points\" -> \"1,000 Points\"\n- \"${payout/100:.2f}\" -> \"$1.00\"\n","examples":["{payout*10:,.0f} Points","${payout/100:.2f}","{payout:.0f}"]},"payout_transformation":{"anyOf":[{"$ref":"#/components/schemas/PayoutTransformation"},{"type":"null"}],"description":"How the BP's payout is converted to the User's payout"}},"type":"object","title":"PayoutConfig","description":"Store configuration related to payouts, payout transformation, and user\npayout formatting."},"PayoutStatus":{"type":"string","enum":["PENDING","APPROVED","REJECTED","CANCELLED","FAILED","COMPLETE"],"title":"PayoutStatus","description":"The max size of the db field that holds this value is 20, so please\ndon't add new values longer than that!"},"PayoutSummary":{"properties":{"min":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Min"},"max":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Max"},"mean":{"anyOf":[{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Mean"},"q1":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Q1"},"q2":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Q2"},"q3":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Q3"},"lower_whisker":{"type":"integer","title":"Lower Whisker","readOnly":true},"upper_whisker":{"type":"integer","title":"Upper Whisker","readOnly":true}},"type":"object","required":["min","max","q1","q2","q3","lower_whisker","upper_whisker"],"title":"PayoutSummary","description":"Payouts are in Integer USD Cents","examples":[{"max":132,"min":14,"q1":45,"q2":68,"q3":124}]},"PayoutTransformation":{"properties":{"f":{"type":"string","enum":["payout_transformation_percent","payout_transformation_amt"],"title":"F","description":"The name of the transformation function to use."},"kwargs":{"anyOf":[{"$ref":"#/components/schemas/PayoutTransformationPercentArgs"},{"type":"null"}],"description":"The kwargs to pass to the transformation function.","examples":[{"max_payout":"5.00","pct":0.5}]}},"type":"object","required":["f"],"title":"PayoutTransformation","description":"This model describe how the bp_payout is converted to the user_payout.\nIf None, the user_payout is None.\n\nIf the user_wallet_enabled is `False`, the user_payout is used to\n 1) know how to transform the expected payouts for offerwall buckets\n (if min_payout is requested, this is based on the user_payout)\n 2) show the user (using the payout_format) how much they made (in\n the Task Status Response).\nIf the user_wallet_enabled is `True`, then in addition to the above, the\n user_payout is the amount actually paid to the user's wallet."},"PayoutTransformationPercentArgs":{"properties":{"pct":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Pct","description":"The percentage of the bp_payout to pay the user","examples":[0.5]},"min_payout":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Min Payout","description":"The minimum amount paid for a complete. Note: This does not check that the actual payout was at least this amount.","examples":["0.50"]},"max_payout":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Max Payout","description":"The maximum amount paid for a complete","examples":["5.00"]}},"type":"object","required":["pct"],"title":"PayoutTransformationPercentArgs"},"PayoutType":{"type":"string","enum":["PAYPAL","TANGO","DWOLLA","ACH","WIRE","CASH_IN_MAIL","PRIZE","AMT","AMT_BONUS","AMT_ASSIGNMENT","AMT_ASSIGNMENT"],"title":"PayoutType","description":"The method in which the requested payout is delivered."},"PaypalCashoutMethodData":{"properties":{"type":{"type":"string","const":"PAYPAL","title":"Type","default":"PAYPAL"},"email":{"type":"string","format":"email","title":"Email","description":"Email address of the paypal user","examples":["test@example.com"]}},"type":"object","required":["email"],"title":"PaypalCashoutMethodData"},"PingMessage":{"properties":{"kind":{"type":"string","const":"ping","title":"Kind","default":"ping"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"}},"type":"object","title":"PingMessage"},"PongMessage":{"properties":{"kind":{"type":"string","const":"pong","title":"Kind","default":"pong"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"}},"type":"object","title":"PongMessage"},"Product":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier of the Brokerage Product","examples":["1108d053e4fa47c5b0dbdcd03a7981e7"]},"name":{"type":"string","maxLength":255,"minLength":3,"title":"Name","description":"Name of the Brokerage Product. Must be unique within a Team","examples":["Website ABC"]},"enabled":{"type":"boolean","title":"Enabled","description":"This is only used to hard block a Product in order to immediately & safely protect against fraud entrances.","default":true},"payments_enabled":{"type":"boolean","title":"Payments Enabled","description":"This is only to determine if ACH or Wire payments should be made to the Produce.","default":true},"created":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created","description":"When the Product was created, this does necessarily mean it started to retrieve traffic at that time."},"team_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Team Id","description":"The organization (group of generalresearch.com admin accounts) that is allowed to modify and manage thisProduct","examples":["b96c1209cf4a4baaa27d38082421a039"]},"business_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Business Id","description":"The legal business entity or individual that is responsible for this account, and that receive SupplierPayments for this Product's activity.","examples":["9b1d71aab7b84112ac936a574fadd5d6"]},"tags":{"items":{"$ref":"#/components/schemas/SupplierTag"},"type":"array","uniqueItems":true,"title":"Tags","description":"Tags which are used to annotate supplier traffic"},"commission_pct":{"type":"string","title":"Commission Pct","default":"0.05"},"redirect_url":{"type":"string","title":"Redirect Url","description":"Where to redirect the user after finishing a session. When a user get redirected back to the supplier, a query param will be added with the name 'tsid', and the value of the TSID for the session. For example: callback_uri: 'https://www.example.com/test/?a=1&b=2' might result in the user getting redirected to: 'https://www.example.com/grl-callback/?a=1&b=2&tsid=c6ab6ba1e75b44e2bf5aab00fc68e3b7'.","examples":["https://www.example.com/grl-callback/?a=1&b=2"]},"harmonizer_domain":{"type":"string","title":"Harmonizer Domain","description":"This is the domain that is used for the GRS (General Research Survey) platform. This is a simple javascript application which may profile the respondent for anyprofiling questions, along with collecting any browserbased security information. The value is a scheme+domain only (no path).","default":"https://profile.generalresearch.com/"},"sources_config":{"anyOf":[{"$ref":"#/components/schemas/SourcesConfig"},{"$ref":"#/components/schemas/SupplyConfig"}],"title":"Sources Config"},"session_config":{"$ref":"#/components/schemas/SessionConfig"},"payout_config":{"$ref":"#/components/schemas/PayoutConfig"},"user_wallet_config":{"$ref":"#/components/schemas/UserWalletConfig"},"user_create_config":{"$ref":"#/components/schemas/UserCreateConfig"},"offerwall_config":{"$ref":"#/components/schemas/OfferwallConfig"},"profiling_config":{"$ref":"#/components/schemas/ProfilingConfig"},"user_health_config":{"$ref":"#/components/schemas/UserHealthConfig"},"yield_man_config":{"$ref":"#/components/schemas/YieldManConfig"},"balance":{"anyOf":[{"$ref":"#/components/schemas/ProductBalances"},{"type":"null"}],"description":"Product Balance"},"payouts_total_str":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payouts Total Str"},"payouts_total":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Payouts Total"},"payouts":{"anyOf":[{"items":{"$ref":"#/components/schemas/BrokerageProductPayoutEvent"},"type":"array"},{"type":"null"}],"title":"Payouts","description":"Product Payouts. These are the ACH or Wire payments that were sent to theBusiness on behalf of this specific Product"},"pop_financial":{"anyOf":[{"items":{"$ref":"#/components/schemas/POPFinancial"},"type":"array"},{"type":"null"}],"title":"Pop Financial"},"bp_account":{"anyOf":[{"$ref":"#/components/schemas/LedgerAccount"},{"type":"null"}]},"user_wallet":{"$ref":"#/components/schemas/UserWalletConfig","readOnly":true}},"type":"object","required":["name","redirect_url","user_wallet"],"title":"Product"},"ProductBalances":{"properties":{"product_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Product Id","examples":["7bb8fd6215c1451399ac80a4a44d4044"]},"last_event":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Event"},"bp_payment_credit":{"type":"integer","minimum":0.0,"title":"Bp Payment Credit","description":"The total amount that has been earned by the Task completes, for this Brokerage Product account.","default":0,"examples":[18837]},"adjustment_credit":{"type":"integer","minimum":0.0,"title":"Adjustment Credit","description":"Positive reconciliations issued back to the Brokerage Product account.","default":0,"examples":[2]},"adjustment_debit":{"type":"integer","minimum":0.0,"title":"Adjustment Debit","description":"Negative reconciliations for any Task completes","default":0,"examples":[753]},"supplier_credit":{"type":"integer","minimum":0.0,"title":"Supplier Credit","description":"ACH or Wire amounts issued to GRL from a Supplier to recoup for a negative Brokerage Product balance","default":0,"examples":[0]},"supplier_debit":{"type":"integer","minimum":0.0,"title":"Supplier Debit","description":"ACH or Wire amounts sent to a Supplier","default":0,"examples":[10000]},"user_bonus_credit":{"type":"integer","minimum":0.0,"title":"User Bonus Credit","description":"If a respondent ever pays back an product account.","default":0,"examples":[0]},"user_bonus_debit":{"type":"integer","minimum":0.0,"title":"User Bonus Debit","description":"Pay a user into their wallet balance. There is no fee here. There is only a fee when the user requests a payout.The bonus could be as a bribe, winnings for a contest, leaderboard, etc.","default":0,"examples":[2745]},"issued_payment":{"type":"integer","minimum":0.0,"title":"Issued Payment","description":"This is the amount that we decide to credit as havingtaken from this Product. If there is any amount not issuedit is summed up over the Business to offset any negativebalances elsewhere.","default":0},"payout":{"type":"integer","title":"Task Payouts","description":"The sum amount of all Task payouts","readOnly":true,"examples":[18837]},"payout_usd_str":{"type":"string","title":"Task Payouts USD Str","readOnly":true,"examples":["$18,837.00"]},"adjustment":{"type":"integer","title":"Task Adjustments","description":"The sum amount of all Task Adjustments","readOnly":true,"examples":[-751]},"expense":{"type":"integer","title":"Product Expenses","description":"The sum amount of any associated Product Expenses (eg: user bonuses)","readOnly":true,"examples":[-2745]},"net":{"type":"integer","title":"Net Earnings","description":"The Product's Net Earnings which is equal to the totalamount of Task Payouts, with Task Adjustments and anyProduct Expenses deducted. This can be positive ornegative.","readOnly":true,"examples":[15341]},"payment":{"type":"integer","minimum":0.0,"title":"Supplier Payments","description":"The sum amount of all Supplier Payments (eg ACH or Wire transfers)","readOnly":true,"examples":[10000]},"payment_usd_str":{"type":"string","title":"Supplier Payments","readOnly":true,"examples":["$10,000"]},"balance":{"type":"integer","title":"Product Balance","description":"The Product's Balance which is equal to the Product's Netamount with already issued Supplier Payments deducted. This can be positive or negative.","readOnly":true,"examples":[5341]},"retainer":{"type":"integer","minimum":0.0,"title":"Smart Retainer","description":"The Smart Retainer is an about of money that is held byGRL to account for any Task Adjustments that may occurin the future. The amount will always be positive, andif the Product's balance is negative, the retainer will be $0.00 as the Product is not eligible for any SupplierPayments either way.","readOnly":true,"examples":[1335]},"retainer_usd_str":{"type":"string","title":"Smart Retainer USD Str","readOnly":true,"examples":["$1,335.00"]},"available_balance":{"type":"integer","minimum":0.0,"title":"Available Balance","description":"The Available Balance is the amount that is currently, andimmediately available for withdraw from the Supplier'sbalance. Supplier Payments are made every Friday for Businesses with an ACH connected Bank Account to GRL, while a Business that requires an International Wire are issued on the last Friday of every Month.","readOnly":true,"examples":[4006]},"available_balance_usd_str":{"type":"string","title":"Available Balance USD Str","readOnly":true,"examples":["$4,006.00"]},"recoup":{"type":"integer","minimum":0.0,"title":"Recoup","readOnly":true,"examples":[282]},"recoup_usd_str":{"type":"string","title":"Recoup Str","readOnly":true,"examples":["$2.04"]},"adjustment_percent":{"type":"number","title":"Adjustment Percentage","description":"The percentage of USDCent value that has been adjustedover all time for this Product.","readOnly":true,"examples":[0.064938]}},"type":"object","required":["payout","payout_usd_str","adjustment","expense","net","payment","payment_usd_str","balance","retainer","retainer_usd_str","available_balance","available_balance_usd_str","recoup","recoup_usd_str","adjustment_percent"],"title":"ProductBalances"},"ProfilingConfig":{"properties":{"enabled":{"type":"boolean","title":"Enabled","description":"If False, the harmonizer/profiling system is not used at all. This should never be False unless special circumstances","default":true},"grs_enabled":{"type":"boolean","title":"Grs Enabled","description":"If grs_enabled is False, and is_grs is passed in the profiling-questions call, \n then don't actually return any questions. This allows a client to hit the endpoint with no limit \n and still get questions. In effect, this means that we'll redirect the user through the GRS\n system but won't present them any questions.","default":true},"n_questions":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"N Questions","description":"Use to hard code the number of questions to ask. None means use default algorithm."},"max_questions":{"type":"integer","exclusiveMinimum":0.0,"title":"Max Questions","description":"The max number of questions we would ask in a session","default":10},"avg_question_count":{"type":"number","exclusiveMinimum":0.0,"title":"Avg Question Count","description":"The average number of questions to ask in a session","default":5},"task_injection_freq_mult":{"type":"number","exclusiveMinimum":0.0,"title":"Task Injection Freq Mult","description":"Scale how frequently we inject profiling questions, relative to the default.1 is default, 2 is twice as often. 10 means always. 0.5 half as often","default":1},"non_us_mult":{"type":"number","exclusiveMinimum":0.0,"title":"Non Us Mult","description":"Non-us multiplier, used to increase freq and length of profilers in all non-us countries.This value is multiplied by task_injection_freq_mult and avg_question_count.","default":2},"hidden_questions_expiration_hours":{"type":"integer","exclusiveMinimum":0.0,"title":"Hidden Questions Expiration Hours","description":"How frequently we should refresh hidden questions","default":168}},"type":"object","title":"ProfilingConfig"},"ProfilingInfoResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"profiling-info":{"items":{"$ref":"#/components/schemas/UpkProperty"},"type":"array","title":"Profiling-Info"}},"type":"object","required":["status","profiling-info"],"title":"ProfilingInfoResponse"},"PropertyType":{"type":"string","enum":["i","n","x"],"title":"PropertyType"},"RaffleUserView":{"properties":{"name":{"type":"string","maxLength":128,"title":"Name","description":"Name of contest. Can be displayed to user."},"description":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Description","description":"Description of contest. Can be displayed to user."},"contest_type":{"type":"string","const":"raffle","title":"Contest Type","default":"raffle"},"end_condition":{"$ref":"#/components/schemas/ContestEndCondition"},"prizes":{"items":{"$ref":"#/components/schemas/ContestPrize"},"type":"array","minItems":1,"title":"Prizes"},"starts_at":{"type":"string","format":"date-time","title":"Starts At","description":"When the contest starts"},"terms_and_conditions":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Terms And Conditions"},"status":{"$ref":"#/components/schemas/ContestStatus","default":"active"},"country_isos":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Isos","description":"Contest is restricted to these countries. If null, all countries are allowed"},"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","description":"Contest applies only to a single BP"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When this contest was created"},"updated_at":{"type":"string","format":"date-time","title":"Updated At","description":"When this contest was last modified. Does not include entries being created/modified"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At","description":"When the contest ended"},"end_reason":{"anyOf":[{"$ref":"#/components/schemas/ContestEndReason"},{"type":"null"}],"description":"The reason the contest ended"},"product_user_id":{"type":"string","title":"Product User Id"},"user_winnings":{"items":{"$ref":"#/components/schemas/ContestWinner"},"type":"array","title":"User Winnings","description":"The prizes won in this contest by the requested user"},"entry_type":{"type":"string","const":"cash","title":"Entry Type","default":"cash"},"entry_rule":{"$ref":"#/components/schemas/ContestEntryRule"},"current_amount":{"anyOf":[{"type":"integer"},{"type":"integer","minimum":0.0}],"title":"Current Amount","description":"Sum of all entry amounts","default":0},"current_participants":{"type":"integer","title":"Current Participants","description":"Count of unique participants","default":0},"user_amount":{"anyOf":[{"type":"integer"},{"type":"integer","minimum":0.0}],"title":"User Amount","description":"The total amount this user has entered"},"user_amount_today":{"anyOf":[{"type":"integer"},{"type":"integer","minimum":0.0}],"title":"User Amount Today","description":"The total amount this user has entered in the past 24 hours"},"current_win_probability":{"type":"number","title":"Current Win Probability","description":"Probability of this user winning 1 or more prizes, if the contestended right now","readOnly":true},"projected_win_probability":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Projected Win Probability","description":"Probability of this user winning 1 or more prizes, once the contestis projected to end. This value is only calculated if the contest has a target_entry_amountend condition.","readOnly":true}},"additionalProperties":false,"type":"object","required":["name","end_condition","product_id","product_user_id","user_amount","user_amount_today","current_win_probability","projected_win_probability"],"title":"RaffleUserView","example":{"contest_type":"raffle","country_isos":"ca,us","created_at":"2025-06-12T21:12:58.061205Z","current_amount":4723,"current_participants":12,"current_win_probability":0.0889265297480415,"description":"iPhone winner will be drawn in proportion to entry amount. Contest ends once $800 has been entered.","end_condition":{"target_entry_amount":80000},"entry_rule":{"max_daily_entries_per_user":1000,"max_entry_amount_per_user":10000},"entry_type":"cash","name":"Win an iPhone","prizes":[{"estimated_cash_value":80000,"kind":"physical","name":"iPhone 16"}],"product_id":"1108d053e4fa47c5b0dbdcd03a7981e7","product_user_id":"test-user","projected_win_probability":0.005249999999999977,"starts_at":"2025-06-12T21:12:58.061170Z","status":"active","updated_at":"2025-06-12T21:12:58.061205Z","user_amount":420,"user_amount_today":0,"user_winnings":[],"uuid":"ce3968b8e18a4b96af62007f262ed7f7"}},"RedemptionCurrency":{"type":"string","enum":["USD","CAD","GBP","EUR","INR","AUD","PLN","SEK","SGD","MXN"],"title":"RedemptionCurrency","description":"Supported Currencies for Foreign Redemptions"},"ReportTask":{"properties":{"bpuid":{"type":"string","maxLength":128,"minLength":3,"title":"product_user_id","description":"The unique identifier for the user, which is set by the Supplier.","examples":["app-user-9329ebd"]},"reasons":{"items":{"$ref":"#/components/schemas/ReportValue"},"type":"array","title":"Reasons","description":"The reason a user reported a task.\n\nAllowed values: \n - __0__ *(REASON_UNKNOWN)*: Used to indicate the user exited the task without giving feedback\n - __1__ *(TECHNICAL_ERROR)*: Task is in the wrong language/country, unanswerable question, won't proceed to next question, loading forever, error message\n - __2__ *(NO_REDIRECT)*: Task ended (completed or failed, and showed the user some dialog indicating the task was over), but failed to redirect\n - __3__ *(PRIVACY_INVASION)*: Asked for full name, home address, identity on another site, cc\n - __4__ *(UNCOMFORTABLE_TOPICS)*: Asked about children, employer, medical issues, drug use, STDs, etc.\n - __5__ *(ASKED_FOR_NOT_ALLOWED_ACTION)*: Asked to install software, signup/login to external site, access webcam, promise to pay using external site, etc.\n - __6__ *(BAD_ON_MOBILE)*: Task doesn't work well on a mobile device\n - __7__ *(DIDNT_LIKE)*: Too long, too boring, confusing, complicated, too many open-ended/free-response questions","examples":[[3,4]]},"notes":{"type":"string","title":"Notes","default":"","examples":["The survey wanted to watch me eat Haejang-guk"]}},"additionalProperties":false,"type":"object","required":["bpuid"],"title":"ReportTask"},"ReportValue":{"type":"integer","enum":[0,1,2,3,4,5,6,7],"title":"ReportValue","description":"The reason a user reported a task."},"Scope":{"type":"string","enum":["global","team","product"],"title":"Scope"},"SessionAdjustedStatus":{"type":"string","enum":["ac","af","pa"],"title":"SessionAdjustedStatus","description":"An adjusted_status is set if a session is adjusted by the marketplace\nafter the original return. A session can be adjusted multiple times.\nThis is the most recent status. If a session was originally a complete,\nwas adjusted to incomplete, then back to complete, the adjusted_status\nwill be None, but the adjusted_timestamp will be set to the most recent\nchange."},"SessionConfig":{"properties":{"max_session_len":{"type":"integer","maximum":5400.0,"minimum":60.0,"title":"Max Session Len","description":"The amount of time (in seconds) that a respondent may spend attempting to get into a survey within a session.If NULL, there is no limit.","default":600},"max_session_hard_retry":{"type":"integer","minimum":0.0,"title":"Max Session Hard Retry","description":"The number of surveys that a respondent may attempt within a session before the session is terminated.","default":5},"min_payout":{"type":"string","title":"Min Payout","description":"The minimum amount the user should be paid for a complete. If \n no payout transformation is defined, the value is based on the BP's payout. \n If a payout transformation is defined, the min_payout is applied on the \n user's payout. Note, this is separate and distinct from the payout \n transformation's min payout. The payout transformation's min_payout does not \n care what the task's actual payout was. This min_payout will prevent\n the user from being show any tasks that would pay below this amount.","default":"0.14","examples":["0.50"]}},"type":"object","title":"SessionConfig","description":"Stores configuration related to the Session, a session being a users\nexperience attempting to do work."},"SessionEnterPayload":{"properties":{"event_type":{"type":"string","const":"session.enter","title":"Event Type","default":"session.enter"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"}},"type":"object","required":["country_iso"],"title":"SessionEnterPayload"},"SessionFinishPayload":{"properties":{"event_type":{"type":"string","const":"session.finish","title":"Event Type","default":"session.finish"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"},"duration_sec":{"type":"number","exclusiveMinimum":0.0,"title":"Duration Sec"},"status":{"$ref":"#/components/schemas/Status"},"status_code_1":{"anyOf":[{"$ref":"#/components/schemas/StatusCode1"},{"type":"null"}]},"status_code_2":{"anyOf":[{"$ref":"#/components/schemas/SessionStatusCode2"},{"type":"null"}]},"user_payout":{"anyOf":[{"type":"integer","maximum":4000.0,"minimum":0.0},{"type":"null"}],"title":"User Payout"}},"type":"object","required":["country_iso","duration_sec","status"],"title":"SessionFinishPayload"},"SessionStatusCode2":{"type":"integer","enum":[1,2,3,4,5,6,7,8,9,10,11,12,13],"title":"SessionStatusCode2","description":"__Status Detail__\nThis should be set if the Session.status_code_1 is SESSION_XXX_FAIL"},"ShippingCarrier":{"type":"string","enum":["USPS","FedEx","UPS","DHL"],"title":"ShippingCarrier"},"ShippingMethod":{"type":"string","enum":["Standard","Express","Two-Day","Overnight","Same Day"],"title":"ShippingMethod"},"SingleEntryBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]}},"additionalProperties":false,"type":"object","required":["id","uri"],"title":"SingleEntryBucket"},"SingleEntryOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/SingleEntryBucket"},"type":"array","maxItems":1,"title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]},"payout_format":{"type":"string","title":"Payout Format","description":"\nThe format describing the str representation of a payout. Typically, this would be displayed to a user.\nThe payout_format is similar to python format string with a subset of functionality supported.\nOnly float with a precision are supported along with an optional comma for a thousands separator.\nIn addition, a mathematical operator can be applied, such as dividing by 100. \nExamples are shown assuming payout = 100 (one dollar).\n- \"{payout*10:,.0f} Points\" -> \"1,000 Points\"\n- \"${payout/100:.2f}\" -> \"$1.00\"\n","examples":["{payout*10:,.0f} Points","${payout/100:.2f}","{payout:.0f}"]}},"additionalProperties":false,"type":"object","required":["id","availability_count","payout_format"],"title":"SingleEntryOfferWall","description":"Only returns a single bucket with the top scoring tasks.\n\nOfferwall code: `5fl8bpv5`"},"SingleEntryOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/SingleEntryOfferWall"}},"type":"object","required":["info","offerwall"],"title":"SingleEntryOfferWallResponse"},"SoftPairBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way. If the eligibility is conditional or ineligible, the uri will be null.","examples":[null]},"category":{"items":{"$ref":"#/components/schemas/CategoryAssociation"},"type":"array","title":"Category"},"contents":{"items":{"$ref":"#/components/schemas/BucketTask"},"type":"array","title":"Contents"},"eligibility":{"type":"string","enum":["conditional","unconditional","ineligible"],"title":"Eligibility","examples":["conditional"]},"missing_questions":{"items":{"type":"string"},"type":"array","title":"Missing Questions","examples":[["fb20fd4773304500b39c4f6de0012a5a"]]},"loi":{"type":"integer","title":"Loi","description":"this is the max loi of the contents","examples":[612]},"payout":{"type":"integer","title":"Payout","description":"this is the min payout of the contents","examples":[123]}},"additionalProperties":false,"type":"object","required":["id","uri","contents","eligibility","loi","payout"],"title":"SoftPairBucket"},"SoftPairOfferwall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/SoftPairBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]},"question_info":{"additionalProperties":{"$ref":"#/components/schemas/UpkQuestion"},"type":"object","title":"Question Info","examples":[]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"SoftPairOfferwall","description":"This offerwall contains tasks for which the user has a conditional\neligibility. The questions that a user must answer to determine the\neligibility are included within each bucket. Additionally, the question\ndefinitions are included for convenience.\n\nOfferwall code: `37d1da64`"},"SoftPairOfferwallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/SoftPairOfferwall"}},"type":"object","required":["info","offerwall"],"title":"SoftPairOfferwallResponse"},"Source":{"type":"string","enum":["g","c","a","d","et","f","i","l","m","n","o","e","r","pr","p","rd","h","s","t","u","w"],"title":"Source"},"SourceConfig":{"properties":{"name":{"$ref":"#/components/schemas/Source"},"active":{"type":"boolean","title":"Active","default":true},"banned_countries":{"items":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},"type":"array","title":"Banned Countries"},"allow_mobile_ip":{"type":"boolean","title":"Allow Mobile Ip","default":true},"allow_pii_only_buyers":{"type":"boolean","title":"Allow Pii Only Buyers","description":"Allow Tasks from Buyers that want traffic that comes from Suppliers that can identify their users. Only supported on Pure Spectrum.","default":false},"allow_unhashed_buyers":{"type":"boolean","title":"Allow Unhashed Buyers","description":"Return Tasks from Buyers that don't have URL hashing enabled. Only supported on Pure Spectrum.","default":false},"withhold_profiling":{"type":"boolean","title":"Withhold Profiling","description":"For some Products, we may have privacy agreements prohibiting us from sharing information with the inventorySource. If True, don't add MRPQ (Market Research ProfilingQuestion) onto the entry link.","default":false},"pass_unconditional_eligible_unknowns":{"type":"boolean","title":"Pass Unconditional Eligible Unknowns","description":"Not used at the moment","default":true}},"type":"object","required":["name"],"title":"SourceConfig","description":"This could also be named \"BP:Marketplace Config\", as it describes the config\nfor a BP on a single marketplace."},"SourcesConfig":{"properties":{"user_defined":{"items":{"$ref":"#/components/schemas/SourceConfig"},"type":"array","title":"User Defined"}},"type":"object","title":"SourcesConfig","description":"Describes the marketplaces or sources that a BP can access and their respective configs,\naka 'BP:Marketplace Configs'"},"StarwallOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]},"payout_format":{"type":"string","title":"Payout Format","description":"\nThe format describing the str representation of a payout. Typically, this would be displayed to a user.\nThe payout_format is similar to python format string with a subset of functionality supported.\nOnly float with a precision are supported along with an optional comma for a thousands separator.\nIn addition, a mathematical operator can be applied, such as dividing by 100. \nExamples are shown assuming payout = 100 (one dollar).\n- \"{payout*10:,.0f} Points\" -> \"1,000 Points\"\n- \"${payout/100:.2f}\" -> \"$1.00\"\n","examples":["{payout*10:,.0f} Points","${payout/100:.2f}","{payout:.0f}"]}},"additionalProperties":false,"type":"object","required":["id","availability_count","payout_format"],"title":"StarwallOfferWall","description":"An offerwall with buckets that are clustered by setting as seeds the\nhighest scoring surveys for each bin, then the rest are distributed\naccording to their Euclidean distance using the bucket's features.\n\nOfferwall code: `b59a2d2b`"},"StarwallOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/StarwallOfferWall"}},"type":"object","required":["info","offerwall"],"title":"StarwallOfferWallResponse"},"StarwallPlusBlockOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"StarwallPlusBlockOfferWall","description":"Same as the StarwallOfferWall, but the buckets include contents and no\nbuckets are returned if the user is blocked.\n\nOfferwall code: `7fa1b3f4`"},"StarwallPlusBlockOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/StarwallPlusBlockOfferWall"}},"type":"object","required":["info","offerwall"],"title":"StarwallPlusBlockOfferWallResponse"},"StarwallPlusBlockRecontactOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusRecontactBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"StarwallPlusBlockRecontactOfferWall","description":"Same as the StarwallOfferWall, but the buckets include contents, no\nbuckets are returned if the user is blocked, and each bucket includes\na recontact key.\n\nOfferwall code: `630db2a4`"},"StarwallPlusBlockRecontactOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/StarwallPlusBlockRecontactOfferWall"}},"type":"object","required":["info","offerwall"],"title":"StarwallPlusBlockRecontactOfferWallResponse"},"StarwallPlusOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"StarwallPlusOfferWall","description":"Same as the StarwallOfferWall, but the buckets include contents.\n\nOfferwall code: `5481f322`"},"StarwallPlusOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/StarwallPlusOfferWall"}},"type":"object","required":["info","offerwall"],"title":"StarwallPlusOfferWallResponse"},"StatisticalSummaryStat":{"properties":{"facet":{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"}]},"propertyNames":{"enum":["country_iso","day","month"]},"type":"object","title":"Facet","description":"The grouping criteria","examples":[{"country_iso":"us"}]},"value":{"$ref":"#/components/schemas/StatisticalSummaryValue","description":"Statistical Summary for the given metric and facet"}},"type":"object","required":["facet","value"],"title":"StatisticalSummaryStat"},"StatisticalSummaryValue":{"properties":{"min":{"type":"number","title":"Min"},"max":{"type":"number","title":"Max"},"mean":{"type":"number","title":"Mean"},"q1":{"type":"number","title":"Q1"},"q2":{"type":"number","title":"Q2","description":"equal to the median"},"q3":{"type":"number","title":"Q3"},"lower_whisker":{"type":"number","title":"Lower Whisker","readOnly":true},"upper_whisker":{"type":"number","title":"Upper Whisker","readOnly":true}},"type":"object","required":["min","max","mean","q1","q2","q3","lower_whisker","upper_whisker"],"title":"StatisticalSummaryValue"},"StatsMessage":{"properties":{"kind":{"type":"string","const":"stats","title":"Kind","default":"stats"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"},"country_iso":{"anyOf":[{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},{"type":"null"}],"title":"Country Iso"},"data":{"$ref":"#/components/schemas/StatsSnapshot"}},"type":"object","required":["data"],"title":"StatsMessage"},"StatsSnapshot":{"properties":{"live_task_count":{"$ref":"#/components/schemas/AggregateBySource"},"task_created_count_last_1h":{"$ref":"#/components/schemas/AggregateBySource"},"task_created_count_last_24h":{"$ref":"#/components/schemas/AggregateBySource"},"live_tasks_max_payout":{"$ref":"#/components/schemas/MaxGaugeBySource","description":"In integer USDCents"},"country_iso":{"anyOf":[{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},{"type":"null"}],"title":"Country Iso"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"},"active_users_last_1h":{"type":"integer","minimum":0.0,"title":"Active Users Last 1H","description":"Count of users (in this product_id) that were active in the past 60 minutes.\n Behaviors that trigger active:\n - Request an offerwall\n - Enter an offerwall bucket\n - Request profiling questions\n - Submit profiling answers\n - Update user profile\n ","default":0},"active_users_last_24h":{"type":"integer","minimum":0.0,"title":"Active Users Last 24H","description":"Count of users (in this product_id) that were active in the past 24 hours.","default":0},"in_progress_users":{"type":"integer","minimum":0.0,"title":"In Progress Users","description":"Count of users that are currently doing work at this moment"},"signups_last_24h":{"type":"integer","minimum":0.0,"title":"Signups Last 24H","description":"Count of users created"},"session_enters_last_1h":{"type":"integer","minimum":0.0,"title":"Session Enters Last 1H"},"session_enters_last_24h":{"type":"integer","minimum":0.0,"title":"Session Enters Last 24H"},"session_fails_last_1h":{"type":"integer","minimum":0.0,"title":"Session Fails Last 1H"},"session_fails_last_24h":{"type":"integer","minimum":0.0,"title":"Session Fails Last 24H"},"session_completes_last_1h":{"type":"integer","minimum":0.0,"title":"Session Completes Last 1H"},"session_completes_last_24h":{"type":"integer","minimum":0.0,"title":"Session Completes Last 24H"},"sum_payouts_last_1h":{"type":"integer","minimum":0.0,"title":"Sum Payouts Last 1H","description":"In integer USDCents"},"sum_payouts_last_24h":{"type":"integer","minimum":0.0,"title":"Sum Payouts Last 24H","description":"In integer USDCents"},"session_avg_payout_last_24h":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Session Avg Payout Last 24H","description":"Average (actual) payout of all tasks completed in the past 24 hrs"},"session_avg_user_payout_last_24h":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Session Avg User Payout Last 24H","description":"Average (actual) user payout of all tasks completed in the past 24 hrs"},"session_fail_avg_loi_last_24h":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Session Fail Avg Loi Last 24H","description":"Average LOI of all tasks terminated in the past 24 hrs (excludes abandons)"},"session_complete_avg_loi_last_24h":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Session Complete Avg Loi Last 24H","description":"Average LOI of all tasks completed in the past 24 hrs"}},"type":"object","required":["in_progress_users","signups_last_24h","session_enters_last_1h","session_enters_last_24h","session_fails_last_1h","session_fails_last_24h","session_completes_last_1h","session_completes_last_24h","sum_payouts_last_1h","sum_payouts_last_24h","session_avg_payout_last_24h","session_avg_user_payout_last_24h","session_fail_avg_loi_last_24h","session_complete_avg_loi_last_24h"],"title":"StatsSnapshot"},"Status":{"type":"string","enum":["c","f","a","t"],"title":"Status","description":"The outcome of a session or wall event. If the session is still in progress, the status will be NULL."},"StatusCode1":{"type":"integer","enum":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19],"title":"StatusCode1","description":"__High level status code for outcome of the session.__\nThis should only be NULL if the Status is ABANDON or TIMEOUT"},"StatusInfoResponseFail":{"properties":{"info":{"$ref":"#/components/schemas/StatusSuccessFail"},"msg":{"type":"string","title":"Msg","description":"An optional message, if success is False","examples":["An error has occurred"]}},"type":"object","required":["info","msg"],"title":"StatusInfoResponseFail"},"StatusResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]}},"type":"object","required":["status"],"title":"StatusResponse"},"StatusResponseFailure":{"properties":{"status":{"type":"string","const":"failure","title":"Status","description":"The status of the API response.","examples":["failure"]},"msg":{"type":"string","title":"Msg","description":"An optional message, if the status is failure.","examples":["An error has occurred"]}},"type":"object","required":["status","msg"],"title":"StatusResponseFailure"},"StatusSuccessFail":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the API response is successful.","default":false}},"type":"object","title":"StatusSuccessFail"},"StreakFulfillment":{"type":"string","enum":["active","complete"],"title":"StreakFulfillment","description":"What has to happen for a user to fulfill a period for a streak"},"StreakPeriod":{"type":"string","enum":["day","week","month"],"title":"StreakPeriod"},"StreakState":{"type":"string","enum":["active","at_risk","broken"],"title":"StreakState"},"SubscribeMessage":{"properties":{"kind":{"type":"string","const":"subscribe","title":"Kind","default":"subscribe"},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","examples":["4fe381fb7186416cb443a38fa66c6557"]}},"type":"object","required":["product_id"],"title":"SubscribeMessage"},"SupplierTag":{"type":"string","enum":["mobile","js-offerwall","double-opt-in","single-sign-on","phone-number-verified","test-a","test-b"],"title":"SupplierTag","description":"Available tags which can be used to annotate supplier traffic\n\nNote: should not include commas!"},"SupplyConfig":{"properties":{"policies":{"items":{"$ref":"#/components/schemas/SupplyPolicy"},"type":"array","title":"Policies"}},"type":"object","title":"SupplyConfig","description":"Describes the set of policies for how GRL can interact with marketplaces.\nThis is only used on the special \"global product\"."},"SupplyPolicy":{"properties":{"name":{"$ref":"#/components/schemas/Source"},"active":{"type":"boolean","title":"Active","default":true},"banned_countries":{"items":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},"type":"array","title":"Banned Countries"},"allow_mobile_ip":{"type":"boolean","title":"Allow Mobile Ip","default":true},"allow_pii_only_buyers":{"type":"boolean","title":"Allow Pii Only Buyers","description":"Allow Tasks from Buyers that want traffic that comes from Suppliers that can identify their users. Only supported on Pure Spectrum.","default":false},"allow_unhashed_buyers":{"type":"boolean","title":"Allow Unhashed Buyers","description":"Return Tasks from Buyers that don't have URL hashing enabled. Only supported on Pure Spectrum.","default":false},"withhold_profiling":{"type":"boolean","title":"Withhold Profiling","description":"For some Products, we may have privacy agreements prohibiting us from sharing information with the inventorySource. If True, don't add MRPQ (Market Research ProfilingQuestion) onto the entry link.","default":false},"pass_unconditional_eligible_unknowns":{"type":"boolean","title":"Pass Unconditional Eligible Unknowns","description":"Not used at the moment","default":true},"address":{"items":{"type":"string"},"type":"array","title":"Address","description":"address for the grpc GetOpps call"},"allow_vpn":{"type":"boolean","title":"Allow Vpn","default":false},"distribute_harmonizer_active":{"type":"boolean","title":"Distribute Harmonizer Active","default":true},"supplier_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Supplier Id","description":"For some inventory Sources, we may partition traffic using different supplier accounts instead"},"team_ids":{"anyOf":[{"items":{"type":"string","maxLength":32,"minLength":32},"type":"array"},{"type":"null"}],"title":"Team Ids"},"product_ids":{"anyOf":[{"items":{"type":"string","maxLength":32,"minLength":32},"type":"array"},{"type":"null"}],"title":"Product Ids"},"integration_mode":{"$ref":"#/components/schemas/IntegrationMode","default":"platform"},"scope":{"$ref":"#/components/schemas/Scope","description":"There must be only 1 GLOBAL config per Source. We can have more than one TEAM/PRODUCT config per Source.","readOnly":true}},"type":"object","required":["name","address","scope"],"title":"SupplyPolicy","description":"One policy describing how GRL can interact with a marketplaces in a certain way.\n This is only used on the special \"global product\", and then internally\n in grpc logic."},"SurveyEligibilityCriterion":{"properties":{"question_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Question Id","examples":["71a367fb71b243dc89f0012e0ec91749"]},"property_code":{"anyOf":[{"type":"string","maxLength":64,"minLength":3,"pattern":"^[a-z]{1,2}\\:.*"},{"type":"null"}],"title":"Property Code","examples":["c:73629"]},"question_text":{"type":"string","title":"Question Text","examples":["What company administers the retirement plan for your current employer?"]},"qualifying_answer":{"items":{"type":"string"},"type":"array","title":"Qualifying Answer","description":"User answer(s) that satisfied at least one eligibility rule","examples":["121"]},"qualifying_answer_label":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Qualifying Answer Label","examples":["Fidelity Investments"]},"explanation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation","description":"Human-readable text explaining how a user's answer to this question affects eligibility","examples":["The company that administers your employer's retirement plan is **Fidelity Investments**."]},"rank":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Rank","description":"Lower values are shown more prominently in the UI"}},"type":"object","required":["question_id","property_code","question_text","qualifying_answer","qualifying_answer_label"],"title":"SurveyEligibilityCriterion","description":"Explanatory record of which question answers contributed\n to a user's eligibility for a survey.\nThis is INSUFFICIENT for determining eligibility to a task\n as it IGNORES logical operators, dependencies between criteria,\n and other requirements. It is only intended for the UI."},"TangoCashoutMethodData":{"properties":{"type":{"type":"string","const":"TANGO","title":"Type","default":"TANGO"},"utid":{"type":"string","title":"Utid","description":"tango utid"},"countries":{"items":{"type":"string"},"type":"array","title":"Countries"},"value_type":{"type":"string","enum":["variable","fixed"],"title":"Value Type"},"disclaimer":{"type":"string","title":"Disclaimer","default":""},"terms":{"type":"string","title":"Terms","default":""}},"type":"object","required":["utid","countries","value_type"],"title":"TangoCashoutMethodData"},"TaskEnterPayload":{"properties":{"event_type":{"type":"string","const":"task.enter","title":"Event Type","default":"task.enter"},"source":{"$ref":"#/components/schemas/Source"},"survey_id":{"type":"string","maxLength":32,"minLength":1,"title":"Survey Id","examples":["127492892"]},"quota_id":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Quota Id","description":"The marketplace's internal quota id"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"}},"type":"object","required":["source","survey_id","country_iso"],"title":"TaskEnterPayload"},"TaskFinishPayload":{"properties":{"event_type":{"type":"string","const":"task.finish","title":"Event Type","default":"task.finish"},"source":{"$ref":"#/components/schemas/Source"},"survey_id":{"type":"string","maxLength":32,"minLength":1,"title":"Survey Id","examples":["127492892"]},"quota_id":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Quota Id","description":"The marketplace's internal quota id"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"},"duration_sec":{"type":"number","exclusiveMinimum":0.0,"title":"Duration Sec"},"status":{"$ref":"#/components/schemas/Status"},"status_code_1":{"anyOf":[{"$ref":"#/components/schemas/StatusCode1"},{"type":"null"}]},"status_code_2":{"anyOf":[{"$ref":"#/components/schemas/WallStatusCode2"},{"type":"null"}]},"cpi":{"anyOf":[{"type":"integer","maximum":4000.0,"minimum":0.0},{"type":"null"}],"title":"Cpi"}},"type":"object","required":["source","survey_id","country_iso","duration_sec","status"],"title":"TaskFinishPayload"},"TaskStatusResponse":{"properties":{"tsid":{"type":"string","maxLength":32,"minLength":32,"title":"Tsid","description":"A unique identifier for the session","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","description":"The BP ID of the associated respondent","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"product_user_id":{"type":"string","maxLength":128,"minLength":3,"title":"Product User Id","description":"A unique identifier for each user, which is set by the Supplier","examples":["app-user-9329ebd"]},"started":{"type":"string","format":"date-time","title":"Started","description":"When the session was started"},"finished":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Finished","description":"When the session was finished"},"status":{"type":"integer","description":"The outcome of a session.\n - 0 - UNKNOWN\n - 1 - ENTER (the user has not yet returned)\n - 2 - INCOMPLETE (the user failed)\n - 3 - COMPLETE (the user completed the task)","examples":[3]},"payout":{"anyOf":[{"type":"integer","exclusiveMaximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Payout","description":"The amount paid to the supplier, in integer USD cents","examples":[500]},"user_payout":{"anyOf":[{"type":"integer","exclusiveMaximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"User Payout","description":"If a payout transformation is configured on this account, this is the amount the user should earn, in integer USD cents","examples":[337]},"payout_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payout Format","description":"\nThe format describing the str representation of a payout. Typically, this would be displayed to a user.\nThe payout_format is similar to python format string with a subset of functionality supported.\nOnly float with a precision are supported along with an optional comma for a thousands separator.\nIn addition, a mathematical operator can be applied, such as dividing by 100. \nExamples are shown assuming payout = 100 (one dollar).\n- \"{payout*10:,.0f} Points\" -> \"1,000 Points\"\n- \"${payout/100:.2f}\" -> \"$1.00\"\n","examples":["{payout*10:,.0f} Points","${payout/100:.2f}","{payout:.0f}"]},"user_payout_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Payout String","description":"If a payout transformation is configured on this account, this is the amount to display to the user","examples":["3370 Points"]},"kwargs":{"additionalProperties":{"type":"string"},"type":"object","title":"Kwargs","description":"Any extra url params used in the offerwall request will be passed back here"},"status_code_1":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"__High level status code for outcome of the session.__\nThis should only be NULL if the Status is ABANDON or TIMEOUT\n\nAllowed values: \n - __BUYER_FAIL__: User terminated in buyer survey\n - __BUYER_QUALITY_FAIL__: User terminated in buyer survey for quality reasons\n - __PS_FAIL__: User failed in marketplace's prescreener\n - __PS_QUALITY__: User rejected by marketplace for quality reasons\n - __PS_BLOCKED__: User is explicitly blocked by the marketplace. Note: on some marketplaces, users can have multiple PS_QUALITY terminations and still complete surveys.\n - __PS_OVERQUOTA__: User rejected by marketplace for over quota\n - __PS_DUPLICATE__: User rejected by marketplace for duplicate\n - __GRS_FAIL__: The user failed within the GRS Platform\n - __GRS_QUALITY_FAIL__: The user failed within the GRS Platform for quality reasons\n - __GRS_ABANDON__: The user abandoned/timed out within the GRS Platform\n - __PS_ABANDON__: The user abandoned/timed out within the marketplace's pre-screen system. Note: On most marketplaces, we have no way of distinguishing between this and BUYER_ABANDON. BUYER_ABANDON is used as the default, unless we know it is PS_ABANDON.\n - __BUYER_ABANDON__: The user abandoned/timed out within the client survey\n - __UNKNOWN__: The status code is not documented\n - __COMPLETE__: The user completed the task successfully\n - __MARKETPLACE_FAIL__: Something was wrong upon the user redirecting from the marketplace, e.g. no postback received, or url hashing failures.\n - __SESSION_START_FAIL__: User failed before being sent into a marketplace\n - __SESSION_CONTINUE_FAIL__: User failed between attempts\n - __SESSION_START_QUALITY_FAIL__: User failed before being sent into a marketplace for \"security\" reasons\n - __SESSION_CONTINUE_QUALITY_FAIL__: User failed between attempts for \"security\" reasons","examples":["COMPLETE"]},"status_code_2":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"__Status Detail__\nThis should be set if the Session.status_code_1 is SESSION_XXX_FAIL\n\nAllowed values: \n - __ENTRY_URL_MODIFICATION__: Unable to parse either the bucket_id, request_id, or nudge_id from the url\n - __UNRECOGNIZED_IP__: The client's IP failed maxmind lookup, or we failed to store it for some reason\n - __USER_IS_ANONYMOUS__: User is using an anonymous IP\n - __USER_IS_BLOCKED__: User is blocked\n - __USER_IS_RATE_LIMITED__: User is rate limited\n - __UNRECOGNIZED_DEVICE__: The client's useragent was not categorized as desktop, mobile, or tablet\n - __OFFERWALL_EXPIRED__: The user clicked after 5 min\n - __INTERNAL_ERROR__: Something unexpected happened\n - __OFFERWALL_COUNTRY_MISMATCH__: The user requested the offerwall for a different country than their IP address indicates\n - __INVALID_BUCKET_ID__: The bucket id indicated in the url does not exist. This is likely due to the user clicking on a bucket for an offerwall that has already been refreshed.\n - __NO_TASKS_AVAILABLE__: Not necessarily the user's fault. We thought we had surveys, but due to for e.g. the user entering on a different device than we thought, there really are none. If we get a lot of these, then that might indicate something is wrong.\n - __ATTEMPT_IS_SUSPICIOUS__: The entrance attempt was flagged by GRLIQ as suspicious\n - __GRLIQ_MISSING__: No GRLIQ forensics post was received","examples":[null]},"adjusted_status":{"anyOf":[{"$ref":"#/components/schemas/SessionAdjustedStatus"},{"type":"null"}],"description":"An adjusted_status is set if a session is adjusted by the marketplace\nafter the original return. A session can be adjusted multiple times.\nThis is the most recent status. If a session was originally a complete,\nwas adjusted to incomplete, then back to complete, the adjusted_status\nwill be None, but the adjusted_timestamp will be set to the most recent\nchange.\n\nAllowed values: \n - __ac__ *(ADJUSTED_TO_COMPLETE)*: Task was reconciled to complete\n - __af__ *(ADJUSTED_TO_FAIL)*: Task was reconciled to incomplete\n - __pa__ *(PAYOUT_ADJUSTMENT)*: The payout was changed. This applies to Sessions ONLY.","examples":[null]},"adjusted_timestamp":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Adjusted Timestamp","description":"When the adjusted status was last set.","examples":[null]},"adjusted_payout":{"anyOf":[{"type":"integer","exclusiveMaximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Adjusted Payout","description":"The new payout after adjustment.","examples":[null]},"adjusted_user_payout":{"anyOf":[{"type":"integer","exclusiveMaximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Adjusted User Payout","description":"The new user_payout after adjustment.","examples":[null]},"adjusted_user_payout_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Adjusted User Payout String","description":"The new user_payout_string after adjustment.","examples":[null]},"wall_events":{"anyOf":[{"items":{"$ref":"#/components/schemas/WallOut"},"type":"array"},{"type":"null"}],"title":"Wall Events"},"currency":{"type":"string","const":"USD","title":"Currency","default":"USD"},"final_status":{"type":"integer","title":"Final Status","description":"This is deprecated","default":0},"bpuid":{"type":"string","title":"Bpuid","readOnly":true}},"type":"object","required":["tsid","product_id","product_user_id","started","bpuid"],"title":"TaskStatusResponse","description":"The status of a session"},"TasksStatusResponse":{"properties":{"page":{"type":"integer","minimum":1.0,"title":"Page","description":"Page number","default":1},"size":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Size","description":"Page size","default":50},"total":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Total","description":"Total number of results"},"tasks_status":{"items":{"$ref":"#/components/schemas/TaskStatusResponse"},"type":"array","title":"Tasks Status"},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages","description":"Total number of pages","readOnly":true}},"type":"object","required":["pages"],"title":"TasksStatusResponse"},"TopNBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"x":{"type":"integer","minimum":0.0,"title":"X","description":"For UI. Provides a dimensionality position for the bucket on the x-axis.","default":0,"examples":[0,1,2]},"y":{"type":"integer","minimum":0.0,"title":"Y","description":"For UI. Provides a dimensionality position for the bucket on the y-axis.","default":0,"examples":[0,1,2]},"name":{"type":"string","title":"Name","description":"Currently unused. Will always return empty string","default":""},"description":{"type":"string","title":"Description","description":"Currently unused. Will always return empty string","default":""},"category":{"items":{"$ref":"#/components/schemas/CategoryAssociation"},"type":"array","title":"Category"},"duration":{"$ref":"#/components/schemas/DurationSummary"},"payout":{"$ref":"#/components/schemas/PayoutSummary"},"quality_score":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Quality Score","description":"A proprietary score to determine the overall quality of the tasks that are within the bucket. Higher is better.","examples":[0.29223]}},"additionalProperties":false,"type":"object","required":["id","uri","duration","payout","quality_score"],"title":"TopNBucket"},"TopNOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]},"payout_format":{"type":"string","title":"Payout Format","description":"\nThe format describing the str representation of a payout. Typically, this would be displayed to a user.\nThe payout_format is similar to python format string with a subset of functionality supported.\nOnly float with a precision are supported along with an optional comma for a thousands separator.\nIn addition, a mathematical operator can be applied, such as dividing by 100. \nExamples are shown assuming payout = 100 (one dollar).\n- \"{payout*10:,.0f} Points\" -> \"1,000 Points\"\n- \"${payout/100:.2f}\" -> \"$1.00\"\n","examples":["{payout*10:,.0f} Points","${payout/100:.2f}","{payout:.0f}"]}},"additionalProperties":false,"type":"object","required":["id","availability_count","payout_format"],"title":"TopNOfferWall","description":"An offerwall with buckets that are clustered by the `split_by` argument\nusing KMeans clustering.\n\nOfferwall code: `45b7228a7`"},"TopNOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/TopNOfferWall"}},"type":"object","required":["info","offerwall"],"title":"TopNOfferWallResponse"},"TopNPlusBlockOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"TopNPlusBlockOfferWall","description":"Same as the TopNOfferWall, but the buckets include contents and no\nbuckets are returned if the user is blocked.\n\nOfferwall code: `d48cce47`"},"TopNPlusBlockOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/TopNPlusBlockOfferWall"}},"type":"object","required":["info","offerwall"],"title":"TopNPlusBlockOfferWallResponse"},"TopNPlusBlockRecontactOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusRecontactBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"TopNPlusBlockRecontactOfferWall","description":"Same as the TopNOfferWall, but the buckets include contents, no buckets\nare returned if the user is blocked, and each bucket includes a\n`is_recontact` key.\n\nOfferwall code: `1e5f0af8`"},"TopNPlusBlockRecontactOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/TopNPlusBlockRecontactOfferWall"}},"type":"object","required":["info","offerwall"],"title":"TopNPlusBlockRecontactOfferWallResponse"},"TopNPlusBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"x":{"type":"integer","minimum":0.0,"title":"X","description":"For UI. Provides a dimensionality position for the bucket on the x-axis.","default":0,"examples":[0,1,2]},"y":{"type":"integer","minimum":0.0,"title":"Y","description":"For UI. Provides a dimensionality position for the bucket on the y-axis.","default":0,"examples":[0,1,2]},"name":{"type":"string","title":"Name","description":"Currently unused. Will always return empty string","default":""},"description":{"type":"string","title":"Description","description":"Currently unused. Will always return empty string","default":""},"category":{"items":{"$ref":"#/components/schemas/CategoryAssociation"},"type":"array","title":"Category"},"contents":{"items":{"$ref":"#/components/schemas/BucketTask"},"type":"array","title":"Contents"},"duration":{"$ref":"#/components/schemas/DurationSummary"},"payout":{"$ref":"#/components/schemas/PayoutSummary"},"quality_score":{"type":"number","title":"Quality Score"},"currency":{"type":"string","title":"Currency","description":"This will always be 'USD'","default":"USD","examples":["USD"]},"eligibility_criteria":{"items":{"$ref":"#/components/schemas/SurveyEligibilityCriterion"},"type":"array","title":"Eligibility Criteria","description":"The reasons the user is eligible for tasks in this bucket"},"eligibility_explanation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Eligibility Explanation","description":"Human-readable text explaining a user's eligibility for tasks in this bucket","examples":["You are a **47-year-old** **white** **male** with a *college degree*, who's employer's retirement plan is **Fidelity Investments**."]}},"additionalProperties":false,"type":"object","required":["id","uri","contents","duration","payout","quality_score"],"title":"TopNPlusBucket"},"TopNPlusOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"TopNPlusOfferWall","description":"Same as the TopNOfferWall, but the buckets include contents.\n\nOfferwall code: `b145b803`"},"TopNPlusOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/TopNPlusOfferWall"}},"type":"object","required":["info","offerwall"],"title":"TopNPlusOfferWallResponse"},"TopNPlusRecontactBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"x":{"type":"integer","minimum":0.0,"title":"X","description":"For UI. Provides a dimensionality position for the bucket on the x-axis.","default":0,"examples":[0,1,2]},"y":{"type":"integer","minimum":0.0,"title":"Y","description":"For UI. Provides a dimensionality position for the bucket on the y-axis.","default":0,"examples":[0,1,2]},"name":{"type":"string","title":"Name","description":"Currently unused. Will always return empty string","default":""},"description":{"type":"string","title":"Description","description":"Currently unused. Will always return empty string","default":""},"category":{"items":{"$ref":"#/components/schemas/CategoryAssociation"},"type":"array","title":"Category"},"contents":{"items":{"$ref":"#/components/schemas/BucketTask"},"type":"array","title":"Contents"},"duration":{"$ref":"#/components/schemas/DurationSummary"},"payout":{"$ref":"#/components/schemas/PayoutSummary"},"quality_score":{"type":"number","title":"Quality Score"},"is_recontact":{"type":"boolean","title":"Is Recontact"},"currency":{"type":"string","title":"Currency","description":"This will always be 'USD'","default":"USD","examples":["USD"]}},"additionalProperties":false,"type":"object","required":["id","uri","contents","duration","payout","quality_score","is_recontact"],"title":"TopNPlusRecontactBucket"},"UPKImportance":{"properties":{"task_count":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Task Count","description":"The number of live Tasks that use this UPK Question","examples":[47]},"task_score":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Task Score","description":"GRL's internal ranked score for the UPK Question","examples":[0.11175522477414712]},"marketplace_task_count":{"anyOf":[{"additionalProperties":{"type":"integer","minimum":0.0},"propertyNames":{"$ref":"#/components/schemas/Source"},"type":"object"},{"type":"null"}],"title":"Marketplace Task Count","description":"The number of live Tasks that use this UPK Question per marketplace","examples":[{"d":23,"s":24}]}},"type":"object","title":"UPKImportance"},"USDeliveryAddress":{"properties":{"name_or_attn":{"type":"string","maxLength":50,"minLength":1,"title":"Name Or Attn"},"company":{"anyOf":[{"type":"string","maxLength":50,"minLength":1},{"type":"null"}],"title":"Company"},"phone_number":{"anyOf":[{"type":"string","maxLength":10,"minLength":10,"pattern":"^[0-9]+$"},{"type":"null"}],"title":"Phone Number"},"address":{"type":"string","maxLength":100,"minLength":1,"title":"Address"},"city":{"type":"string","maxLength":100,"minLength":1,"title":"City"},"state":{"type":"string","maxLength":2,"minLength":1,"title":"State"},"postal_code":{"type":"string","maxLength":10,"minLength":1,"title":"Postal Code"},"country":{"type":"string","title":"Country","default":"us"}},"type":"object","required":["name_or_attn","address","city","state","postal_code"],"title":"USDeliveryAddress"},"UpkItem":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","examples":["497b1fedec464151b063cd5367643ffa"]},"label":{"type":"string","maxLength":255,"title":"Label","examples":["high_school_completion"]},"description":{"anyOf":[{"type":"string","maxLength":1024},{"type":"null"}],"title":"Description","examples":["Completed high school"]}},"type":"object","required":["id","label"],"title":"UpkItem"},"UpkProperty":{"properties":{"property_id":{"type":"string","maxLength":32,"minLength":32,"title":"Property Id","examples":["2af5240ecd7d4a909a6caf5824bf4632"]},"property_label":{"type":"string","maxLength":255,"title":"Property Label","examples":["educational_attainment"]},"prop_type":{"$ref":"#/components/schemas/PropertyType","description":"\nAllowed values: \n - __i__ *(UPK_ITEM)*: UserProfileKnowledge Item\n - __n__ *(UPK_NUMERICAL)*: UserProfileKnowledge Numerical\n - __x__ *(UPK_TEXT)*: UserProfileKnowledge Text","default":"i"},"cardinality":{"$ref":"#/components/schemas/Cardinality","description":"\nAllowed values: \n - __*__ *(ZERO_OR_MORE)*: Zero or More\n - __?__ *(ZERO_OR_ONE)*: Zero or One","default":"?"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"},"gold_standard":{"type":"boolean","title":"Gold Standard","description":"A Gold-Standard question has been enumerated for all possible values (per country) as best as possible by GRL,allowing it to be mapped across inventory sources. A property not marked as Gold-Standard may have: 1) marketplace qid associations & 2) category associations, but doesn't have a defined 'range' (list of allowed itemsin a multiple choice question). This is used for exposing a user's profiling data & forthe Nudge API.","default":false},"allowed_items":{"anyOf":[{"items":{"$ref":"#/components/schemas/UpkItem"},"type":"array"},{"type":"null"}],"title":"Allowed Items"},"categories":{"items":{"$ref":"#/components/schemas/Category"},"type":"array","title":"Categories"}},"type":"object","required":["property_id","property_label","country_iso"],"title":"UpkProperty","description":"This used to be called \"QuestionInfo\", which is a bad name,\n as this describes a UPK Property, like \"educational_attainment\",\n not the question that asks for your education."},"UpkQuestion":{"properties":{"question_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Question Id"},"ext_question_id":{"anyOf":[{"type":"string","pattern":"^[a-z]{1,2}\\:.*"},{"type":"null"}],"title":"Ext Question Id","description":"what marketplace question this question links to"},"question_type":{"$ref":"#/components/schemas/UpkQuestionType"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"},"language_iso":{"type":"string","maxLength":3,"minLength":3,"pattern":"^[a-z]{3}$","title":"Language Iso"},"question_text":{"type":"string","minLength":1,"title":"Question Text","description":"The text shown to respondents"},"choices":{"anyOf":[{"items":{"$ref":"#/components/schemas/UpkQuestionChoice"},"type":"array"},{"type":"null"}],"title":"Choices"},"selector":{"anyOf":[{"$ref":"#/components/schemas/UpkQuestionSelectorMC"},{"$ref":"#/components/schemas/UpkQuestionSelectorTE"},{"$ref":"#/components/schemas/UpkQuestionSelectorSLIDER"},{"$ref":"#/components/schemas/UpkQuestionSelectorHIDDEN"}],"title":"Selector"},"configuration":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/UpkQuestionConfigurationMC"},{"$ref":"#/components/schemas/UpkQuestionConfigurationTE"},{"$ref":"#/components/schemas/UpkQuestionConfigurationSLIDER"}],"discriminator":{"propertyName":"type","mapping":{"MC":"#/components/schemas/UpkQuestionConfigurationMC","SLIDER":"#/components/schemas/UpkQuestionConfigurationSLIDER","TE":"#/components/schemas/UpkQuestionConfigurationTE"}}},{"type":"null"}],"title":"Configuration"},"validation":{"anyOf":[{"$ref":"#/components/schemas/UpkQuestionValidation"},{"type":"null"}]},"importance":{"anyOf":[{"$ref":"#/components/schemas/UPKImportance"},{"type":"null"}]},"categories":{"items":{"$ref":"#/components/schemas/Category"},"type":"array","title":"Categories","description":"Categories associated with this question"},"explanation_template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation Template","description":"Human-readable template for explaining how a user's answer to this question affects eligibility","examples":["The company that administers your employer's retirement plan is {answer}."]},"explanation_fragment_template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation Fragment Template","description":"A very short, natural-language explanation fragment that can be combined with others into a single sentence","examples":["whose employer's retirement plan is {answer}"]}},"type":"object","required":["question_type","country_iso","language_iso","question_text"],"title":"UpkQuestion","example":{"categories":[{"adwords_vertical_id":"396","label":"Politics","parent_uuid":"f66dddba61424ce5be2a38731450a0e1","path":"/News/Politics","uuid":"87b6d819f3ca4815bf1f135b1e829cc6"}],"choices":[{"choice_id":"1","choice_text":"Yes","exclusive":false,"order":0},{"choice_id":"2","choice_text":"No","exclusive":false,"order":1}],"country_iso":"us","importance":{"marketplace_task_count":{"d":9,"s":20,"w":20},"task_count":49,"task_score":3.3401743283265684},"language_iso":"eng","marketplace_task_count":{"d":9,"s":20,"w":20},"question_id":"fb20fd4773304500b39c4f6de0012a5a","question_text":"Are you registered to vote at your present address, or not?","question_type":"MC","selector":"SA","task_count":49,"task_score":3.3401743283265684}},"UpkQuestionChoice":{"properties":{"choice_id":{"type":"string","maxLength":64,"minLength":1,"pattern":"^[\\w\\s\\.\\-]+$","title":"Choice Id","description":"The unique identifier for a response to a qualification"},"choice_text":{"type":"string","minLength":1,"title":"Choice Text","description":"The response text shown to respondents"},"order":{"type":"integer","minimum":0.0,"title":"Order"},"group":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Group"},"exclusive":{"type":"boolean","title":"Exclusive","description":"If answer is exclusive, it can be the only option selected","default":false},"importance":{"anyOf":[{"$ref":"#/components/schemas/UPKImportance"},{"type":"null"}]}},"type":"object","required":["choice_id","choice_text","order"],"title":"UpkQuestionChoice"},"UpkQuestionChoiceOut":{"properties":{"choice_id":{"type":"string","maxLength":64,"minLength":1,"pattern":"^[\\w\\s\\.\\-]+$","title":"Choice Id","description":"The unique identifier for a response to a qualification"},"choice_text":{"type":"string","minLength":1,"title":"Choice Text","description":"The response text shown to respondents"},"order":{"type":"integer","minimum":0.0,"title":"Order"},"group":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Group"},"exclusive":{"type":"boolean","title":"Exclusive","description":"If answer is exclusive, it can be the only option selected","default":false},"importance":{"anyOf":[{"$ref":"#/components/schemas/UPKImportance"},{"type":"null"}]}},"type":"object","required":["choice_id","choice_text","order"],"title":"UpkQuestionChoiceOut"},"UpkQuestionConfigurationMC":{"properties":{"max_select":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Max Select"}},"additionalProperties":false,"type":"object","title":"UpkQuestionConfigurationMC"},"UpkQuestionConfigurationSLIDER":{"properties":{"slider_min":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Slider Min"},"slider_max":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Slider Max"},"slider_start":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Slider Start"},"slider_step":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Slider Step"}},"type":"object","title":"UpkQuestionConfigurationSLIDER"},"UpkQuestionConfigurationTE":{"properties":{"max_length":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Max Length","description":"Maximum str length of any input. Meant as an easy, nonregex based check."},"min_length":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Min Length","description":"Minimum str length of any input. Meant as an easy, nonregex based check."}},"additionalProperties":false,"type":"object","title":"UpkQuestionConfigurationTE"},"UpkQuestionOut":{"properties":{"question_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Question Id"},"ext_question_id":{"anyOf":[{"type":"string","pattern":"^[a-z]{1,2}\\:.*"},{"type":"null"}],"title":"Ext Question Id","description":"what marketplace question this question links to"},"question_type":{"$ref":"#/components/schemas/UpkQuestionType"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"},"language_iso":{"type":"string","maxLength":3,"minLength":3,"pattern":"^[a-z]{3}$","title":"Language Iso"},"question_text":{"type":"string","minLength":1,"title":"Question Text","description":"The text shown to respondents"},"choices":{"anyOf":[{"items":{"$ref":"#/components/schemas/UpkQuestionChoiceOut"},"type":"array"},{"type":"null"}],"title":"Choices"},"selector":{"anyOf":[{"$ref":"#/components/schemas/UpkQuestionSelectorMC"},{"$ref":"#/components/schemas/UpkQuestionSelectorTE"},{"$ref":"#/components/schemas/UpkQuestionSelectorSLIDER"},{"$ref":"#/components/schemas/UpkQuestionSelectorHIDDEN"}],"title":"Selector"},"configuration":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/UpkQuestionConfigurationMC"},{"$ref":"#/components/schemas/UpkQuestionConfigurationTE"},{"$ref":"#/components/schemas/UpkQuestionConfigurationSLIDER"}],"discriminator":{"propertyName":"type","mapping":{"MC":"#/components/schemas/UpkQuestionConfigurationMC","SLIDER":"#/components/schemas/UpkQuestionConfigurationSLIDER","TE":"#/components/schemas/UpkQuestionConfigurationTE"}}},{"type":"null"}],"title":"Configuration"},"validation":{"anyOf":[{"$ref":"#/components/schemas/UpkQuestionValidation"},{"type":"null"}]},"importance":{"anyOf":[{"$ref":"#/components/schemas/UPKImportance"},{"type":"null"}]},"categories":{"items":{"$ref":"#/components/schemas/Category"},"type":"array","title":"Categories","description":"Categories associated with this question"},"explanation_template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation Template","description":"Human-readable template for explaining how a user's answer to this question affects eligibility","examples":["The company that administers your employer's retirement plan is {answer}."]},"explanation_fragment_template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation Fragment Template","description":"A very short, natural-language explanation fragment that can be combined with others into a single sentence","examples":["whose employer's retirement plan is {answer}"]},"task_count":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Task Count","description":"The number of live Tasks that use this UPK Question","examples":[47]},"task_score":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Task Score","description":"GRL's internal ranked score for the UPK Question","examples":[0.11175522477414712]},"marketplace_task_count":{"anyOf":[{"additionalProperties":{"type":"integer","minimum":0.0},"propertyNames":{"$ref":"#/components/schemas/Source"},"type":"object"},{"type":"null"}],"title":"Marketplace Task Count","description":"The number of live Tasks that use this UPK Question per marketplace","examples":[{"d":23,"s":24}]}},"type":"object","required":["question_type","country_iso","language_iso","question_text"],"title":"UpkQuestionOut","example":{"categories":[{"adwords_vertical_id":"396","label":"Politics","parent_uuid":"f66dddba61424ce5be2a38731450a0e1","path":"/News/Politics","uuid":"87b6d819f3ca4815bf1f135b1e829cc6"}],"choices":[{"choice_id":"1","choice_text":"Yes","exclusive":false,"order":0},{"choice_id":"2","choice_text":"No","exclusive":false,"order":1}],"country_iso":"us","importance":{"marketplace_task_count":{"d":9,"s":20,"w":20},"task_count":49,"task_score":3.3401743283265684},"language_iso":"eng","marketplace_task_count":{"d":9,"s":20,"w":20},"question_id":"fb20fd4773304500b39c4f6de0012a5a","question_text":"Are you registered to vote at your present address, or not?","question_type":"MC","selector":"SA","task_count":49,"task_score":3.3401743283265684}},"UpkQuestionResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"questions":{"items":{"$ref":"#/components/schemas/UpkQuestionOut"},"type":"array","title":"Questions"},"consent_questions":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Consent Questions","description":"For internal use"},"special_questions":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Special Questions","description":"For internal use"},"count":{"type":"integer","minimum":0.0,"title":"Count","description":"The number of questions returned"}},"type":"object","required":["status","questions","count"],"title":"UpkQuestionResponse"},"UpkQuestionSelectorHIDDEN":{"type":"string","enum":["HIDDEN"],"title":"UpkQuestionSelectorHIDDEN"},"UpkQuestionSelectorMC":{"type":"string","enum":["SA","MA","DL","SB","MSB"],"title":"UpkQuestionSelectorMC"},"UpkQuestionSelectorSLIDER":{"type":"string","enum":["HSLIDER","VSLIDER"],"title":"UpkQuestionSelectorSLIDER"},"UpkQuestionSelectorTE":{"type":"string","enum":["SL","ML","ETB"],"title":"UpkQuestionSelectorTE"},"UpkQuestionType":{"type":"string","enum":["MC","TE","SLIDER","HIDDEN"],"title":"UpkQuestionType"},"UpkQuestionValidation":{"properties":{"patterns":{"anyOf":[{"items":{"$ref":"#/components/schemas/PatternValidation"},"type":"array","minItems":1},{"type":"null"}],"title":"Patterns"}},"type":"object","required":["patterns"],"title":"UpkQuestionValidation"},"User":{"properties":{"id":{"anyOf":[{"type":"integer","exclusiveMaximum":2147483648.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Id"},"uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Uuid","examples":["8b33426cdc8548708b52b8389ca99125"]},"product":{"anyOf":[{"$ref":"#/components/schemas/Product"},{"type":"null"}]},"product_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Product Id","examples":["4fe381fb7186416cb443a38fa66c6557"]},"product_user_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":3},{"type":"null"}],"title":"Product User Id","description":"A unique identifier for each user, which is set by the Supplier. It should not contain any sensitive informationlike email or names, and should avoid using anyincrementing values.","examples":["app-user-9329ebd"]},"blocked":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Blocked","default":false},"created":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created","description":"When the user was created on the GRL platform."},"last_seen":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Seen","description":"When the user was last seen on, or acting on anypart of the GRL platform."},"audit_log":{"anyOf":[{"items":{"$ref":"#/components/schemas/AuditLog"},"type":"array"},{"type":"null"}],"title":"Audit Log"},"transactions":{"anyOf":[{"items":{"$ref":"#/components/schemas/LedgerTransaction"},"type":"array"},{"type":"null"}],"title":"Transactions"},"location_history":{"anyOf":[{"items":{"$ref":"#/components/schemas/GeoIPInformation"},"type":"array"},{"type":"null"}],"title":"Location History"}},"additionalProperties":false,"type":"object","title":"User"},"UserCreateConfig":{"properties":{"min_hourly_create_limit":{"type":"integer","minimum":0.0,"title":"Min Hourly Create Limit","description":"The smallest allowed value for the hourly user create limit.","default":0},"max_hourly_create_limit":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Max Hourly Create Limit","description":"The largest allowed value for the hourly user create limit. If None, the hourly create limit is unconstrained."}},"type":"object","title":"UserCreateConfig","description":"Stores configuration for the user creation experience.\n\nThe user creation limit is determined dynamically based on the median\n daily completion rate. min_hourly_create_limit &\n max_hourly_create_limit can be used to constrain the dynamically\n determined rate limit within set values."},"UserHealthConfig":{"properties":{"banned_countries":{"items":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},"type":"array","title":"Banned Countries"},"allow_ban_iphist":{"type":"boolean","title":"Allow Ban Iphist","default":true}},"type":"object","title":"UserHealthConfig"},"UserInfo":{"properties":{"user_profile_knowledge":{"items":{"$ref":"#/components/schemas/UserProfileKnowledge"},"type":"array","title":"User Profile Knowledge"},"marketplace_profile_knowledge":{"items":{"$ref":"#/components/schemas/MarketProfileKnowledge"},"type":"array","title":"Marketplace Profile Knowledge"}},"type":"object","title":"UserInfo"},"UserInfoResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"user_profile":{"$ref":"#/components/schemas/UserInfo"}},"type":"object","required":["status","user_profile"],"title":"UserInfoResponse"},"UserLedgerTransactionTaskAdjustment":{"properties":{"created":{"type":"string","format":"date-time","title":"Created","description":"When the Transaction was created"},"description":{"type":"string","maxLength":255,"title":"Description","description":"External description suitable for UI","default":"Task Adjustment"},"amount":{"type":"integer","title":"Amount","description":"The net amount affecting the user's wallet, in USDCents. Positive means the user's balance increased; negative means it decreased.","examples":[500,-250]},"balance_after":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Balance After"},"tx_type":{"type":"string","const":"bp_adjustment","title":"Tx Type","default":"bp_adjustment"},"tsid":{"type":"string","maxLength":32,"minLength":32,"title":"Tsid","description":"A unique identifier for the session","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"A link to where the user can get more details about this transaction","readOnly":true},"amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amount String","description":"The 'amount' with the payout_format applied.","readOnly":true}},"additionalProperties":false,"type":"object","required":["created","amount","tsid","url","amount_string"],"title":"UserLedgerTransactionTaskAdjustment","example":{"amount":-38,"amount_string":"$-0.38","created":"2026-02-18T07:03:18.644340Z","description":"Task Adjustment","tsid":"25d6081840694376b581f392653ac6fd","tx_type":"bp_adjustment","url":"https://fsb.generalresearch.com/d911167a994a470dbd3ded8b26ff431a/status/25d6081840694376b581f392653ac6fd/"}},"UserLedgerTransactionTaskComplete":{"properties":{"created":{"type":"string","format":"date-time","title":"Created","description":"When the Transaction was created"},"description":{"type":"string","maxLength":255,"title":"Description","description":"External description suitable for UI","default":"Task Complete"},"amount":{"type":"integer","title":"Amount","description":"The net amount affecting the user's wallet, in USDCents. Positive means the user's balance increased; negative means it decreased.","examples":[500,-250]},"balance_after":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Balance After"},"tx_type":{"type":"string","const":"bp_payment","title":"Tx Type","default":"bp_payment"},"tsid":{"type":"string","maxLength":32,"minLength":32,"title":"Tsid","description":"A unique identifier for the session","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"A link to where the user can get more details about this transaction","readOnly":true},"amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amount String","description":"The 'amount' with the payout_format applied.","readOnly":true}},"additionalProperties":false,"type":"object","required":["created","amount","tsid","url","amount_string"],"title":"UserLedgerTransactionTaskComplete","description":"In a BP with user wallet enabled, the task-complete transaction would have line items\n for both the credit to the bp_wallet_account and credit to user_account. This\n is the user-detail, so we've only caring about the user's payment.","example":{"amount":38,"amount_string":"$0.38","created":"2026-02-18T07:03:18.644965Z","description":"Task Complete","tsid":"701ce08adc6d475487fba63412811133","tx_type":"bp_payment","url":"https://fsb.generalresearch.com/40894c77ac64432a994c00c1bf997bda/status/701ce08adc6d475487fba63412811133/"}},"UserLedgerTransactionTypeSummary":{"properties":{"entry_count":{"type":"integer","minimum":0.0,"title":"Entry Count","default":0},"min_amount":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Amount","description":"positive or negative USDCent"},"max_amount":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Amount","description":"positive or negative USDCent"},"total_amount":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Amount","description":"positive or negative USDCent"}},"type":"object","title":"UserLedgerTransactionTypeSummary"},"UserLedgerTransactionTypesSummary":{"properties":{"bp_adjustment":{"$ref":"#/components/schemas/UserLedgerTransactionTypeSummary"},"bp_payment":{"$ref":"#/components/schemas/UserLedgerTransactionTypeSummary"},"user_bonus":{"$ref":"#/components/schemas/UserLedgerTransactionTypeSummary"},"user_payout_request":{"$ref":"#/components/schemas/UserLedgerTransactionTypeSummary"}},"type":"object","title":"UserLedgerTransactionTypesSummary"},"UserLedgerTransactionUserBonus":{"properties":{"created":{"type":"string","format":"date-time","title":"Created","description":"When the Transaction was created"},"description":{"type":"string","maxLength":255,"title":"Description","description":"External description suitable for UI","default":"Compensation Bonus"},"amount":{"type":"integer","title":"Amount","description":"The net amount affecting the user's wallet, in USDCents. Positive means the user's balance increased; negative means it decreased.","examples":[500,-250]},"balance_after":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Balance After"},"tx_type":{"type":"string","const":"user_bonus","title":"Tx Type","default":"user_bonus"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"A link to where the user can get more details about this transaction","readOnly":true},"amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amount String","description":"The 'amount' with the payout_format applied.","readOnly":true}},"additionalProperties":false,"type":"object","required":["created","amount","url","amount_string"],"title":"UserLedgerTransactionUserBonus","example":{"amount":100,"amount_string":"$1.00","created":"2026-02-18T07:03:18.643773Z","description":"Compensation Bonus","tx_type":"user_bonus"}},"UserLedgerTransactionUserPayout":{"properties":{"created":{"type":"string","format":"date-time","title":"Created","description":"When the Transaction was created"},"description":{"type":"string","maxLength":255,"title":"Description","description":"External description suitable for UI"},"amount":{"type":"integer","title":"Amount","description":"The net amount affecting the user's wallet, in USDCents. Positive means the user's balance increased; negative means it decreased.","examples":[500,-250]},"balance_after":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Balance After"},"tx_type":{"type":"string","const":"user_payout_request","title":"Tx Type","default":"user_payout_request"},"payout_id":{"type":"string","maxLength":32,"minLength":32,"title":"Payout Id","description":"A unique identifier for the payout","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"A link to where the user can get more details about this transaction","readOnly":true},"amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amount String","description":"The 'amount' with the payout_format applied.","readOnly":true}},"additionalProperties":false,"type":"object","required":["created","description","amount","payout_id","url","amount_string"],"title":"UserLedgerTransactionUserPayout","example":{"amount":-5,"amount_string":"$-0.05","created":"2026-02-18T07:03:18.643220Z","description":"HIT Reward","payout_id":"6734d18098a745719c71ccd36e4cbff1","tx_type":"user_payout_request","url":"https://fsb.generalresearch.com/fd768560e9ec4f77aa3d697d9961aa97/cashout/6734d18098a745719c71ccd36e4cbff1/"}},"UserLedgerTransactionsResponse":{"properties":{"page":{"type":"integer","minimum":1.0,"title":"Page","description":"Page number","default":1},"size":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Size","description":"Page size","default":50},"total":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Total","description":"Total number of results"},"transactions":{"items":{"oneOf":[{"$ref":"#/components/schemas/UserLedgerTransactionUserPayout"},{"$ref":"#/components/schemas/UserLedgerTransactionUserBonus"},{"$ref":"#/components/schemas/UserLedgerTransactionTaskAdjustment"},{"$ref":"#/components/schemas/UserLedgerTransactionTaskComplete"}],"discriminator":{"propertyName":"tx_type","mapping":{"bp_adjustment":"#/components/schemas/UserLedgerTransactionTaskAdjustment","bp_payment":"#/components/schemas/UserLedgerTransactionTaskComplete","user_bonus":"#/components/schemas/UserLedgerTransactionUserBonus","user_payout_request":"#/components/schemas/UserLedgerTransactionUserPayout"}}},"type":"array","title":"Transactions"},"summary":{"$ref":"#/components/schemas/UserLedgerTransactionTypesSummary"},"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages","description":"Total number of pages","readOnly":true}},"type":"object","required":["summary","status","pages"],"title":"UserLedgerTransactionsResponse"},"UserProfile":{"properties":{"email_address":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email Address","examples":["contact@mail.com"]},"user":{"$ref":"#/components/schemas/User"},"marketplace_pids":{"additionalProperties":{"type":"string","maxLength":32,"minLength":32},"propertyNames":{"$ref":"#/components/schemas/Source"},"type":"object","title":"Marketplace Pids","description":"User's PID in marketplaces","examples":[{"c":"b507a2c00c3e481fb82f23655d142198","d":"deffe922063e4b9980206a62c3df2fba","i":"1dd9bd986794444eb97cb921aee5663f"}]},"streaks":{"items":{"$ref":"#/components/schemas/UserStreak"},"type":"array","title":"Streaks"},"email_md5":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Email Md5","description":"MD5 hash of the email address","readOnly":true,"examples":["053fc3d5575362159e0c782abec83ffa"]},"email_sha1":{"anyOf":[{"type":"string","maxLength":40,"minLength":40},{"type":"null"}],"title":"Email Sha1","description":"SHA1 hash of the email address","readOnly":true,"examples":["6280fb76135b3585c0c5403be04844a0f0bae726"]},"email_sha256":{"anyOf":[{"type":"string","maxLength":64,"minLength":64},{"type":"null"}],"title":"Email Sha256","description":"SHA256 hash of the email address","readOnly":true,"examples":["8a098233e750f08de87d6053c06a58724287f34372368b6dc28b7ad4a77f3d39"]}},"additionalProperties":false,"type":"object","required":["user","email_md5","email_sha1","email_sha256"],"title":"UserProfile"},"UserProfileKnowledge":{"properties":{"property_id":{"type":"string","title":"Property Id"},"property_label":{"type":"string","title":"Property Label"},"translation":{"type":"string","title":"Translation"},"answer":{"items":{"$ref":"#/components/schemas/UserProfileKnowledgeAnswer"},"type":"array","title":"Answer"},"created":{"type":"string","format":"date-time","title":"Created","description":"When the User submitted this Profiling data"}},"type":"object","required":["property_id","property_label","translation","created"],"title":"UserProfileKnowledge"},"UserProfileKnowledgeAnswer":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"translation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Translation"},"value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Value"}},"type":"object","title":"UserProfileKnowledgeAnswer"},"UserProfileResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"user_profile":{"$ref":"#/components/schemas/UserProfile"}},"type":"object","required":["status","user_profile"],"title":"UserProfileResponse"},"UserProfileUpdateRequest":{"properties":{"email_address":{"type":"string","format":"email","title":"Email Address","examples":["contact@mail.com"]}},"additionalProperties":false,"type":"object","required":["email_address"],"title":"UserProfileUpdateRequest"},"UserProfilesResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"user_profiles":{"items":{"$ref":"#/components/schemas/UserProfile"},"type":"array","title":"User Profiles"}},"type":"object","required":["status","user_profiles"],"title":"UserProfilesResponse"},"UserQuestionAnswerIn":{"properties":{"question_id":{"type":"string","maxLength":32,"minLength":32,"title":"Question Id","examples":["fb20fd4773304500b39c4f6de0012a5a"]},"answer":{"items":{"type":"string","maxLength":5000,"minLength":1},"type":"array","maxItems":10,"minItems":1,"title":"Answer","description":"The user's answers to this question. Must pass the choice_id if the question is a Multiple Choice, or the actual text if the question is Text Entry","examples":[["1"]]}},"additionalProperties":false,"type":"object","required":["question_id","answer"],"title":"UserQuestionAnswerIn","description":"Send the answers to one or more questions for a user. A question is\nuniquely specified by the question_id key. The answer is: the choice_id\nif the question_type is \"MC\" the actual entered text if the\nquestion_type is \"TE\"\n\nTODO: look up the question_type from the question_id to apply MC or\n TE specific validation on the answer(s)"},"UserStreak":{"properties":{"country_iso":{"type":"string","title":"Country Iso"},"period":{"$ref":"#/components/schemas/StreakPeriod"},"fulfillment":{"$ref":"#/components/schemas/StreakFulfillment"},"current_streak":{"type":"integer","minimum":0.0,"title":"Current Streak"},"longest_streak":{"type":"integer","minimum":0.0,"title":"Longest Streak"},"state":{"$ref":"#/components/schemas/StreakState"},"last_fulfilled_period_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Last Fulfilled Period Start"},"timezone_name":{"type":"string","title":"Timezone Name","readOnly":true},"current_period_bounds":{"prefixItems":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}],"type":"array","maxItems":2,"minItems":2,"title":"Current Period Bounds","readOnly":true},"last_fulfilled_period_bounds":{"anyOf":[{"prefixItems":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}],"type":"array","maxItems":2,"minItems":2},{"type":"null"}],"title":"Last Fulfilled Period Bounds","readOnly":true},"time_remaining_in_period":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Time Remaining In Period","readOnly":true}},"additionalProperties":false,"type":"object","required":["country_iso","period","fulfillment","current_streak","longest_streak","state","timezone_name","current_period_bounds","last_fulfilled_period_bounds","time_remaining_in_period"],"title":"UserStreak"},"UserType":{"type":"string","enum":["business","cafe","cellular","college","content_delivery_network","consumer_privacy_network","government","hosting","library","military","residential","router","school","search_engine_spider","traveler"],"title":"UserType"},"UserWalletBalance":{"properties":{"amount":{"type":"integer","title":"Amount","description":"(USD cents) The amount in the user's wallet."},"redeemable_amount":{"type":"integer","minimum":0.0,"title":"Redeemable Amount","description":"(USD cents) The amount in the user's wallet this is currently redeemable."},"payout_format":{"type":"string","title":"Payout Format","description":"\nThe format describing the str representation of a payout. Typically, this would be displayed to a user.\nThe payout_format is similar to python format string with a subset of functionality supported.\nOnly float with a precision are supported along with an optional comma for a thousands separator.\nIn addition, a mathematical operator can be applied, such as dividing by 100. \nExamples are shown assuming payout = 100 (one dollar).\n- \"{payout*10:,.0f} Points\" -> \"1,000 Points\"\n- \"${payout/100:.2f}\" -> \"$1.00\"\n","examples":["{payout*10:,.0f} Points","${payout/100:.2f}","{payout:.0f}"]},"amount_string":{"type":"string","title":"Amount String","description":"The 'amount' with the payout_format applied. Can be displayed to the user."},"redeemable_amount_string":{"type":"string","title":"Redeemable Amount String","description":"The 'redeemable_amount' with the payout_format applied. Can be displayed to the user."}},"type":"object","required":["amount","redeemable_amount","payout_format","amount_string","redeemable_amount_string"],"title":"UserWalletBalance","example":{"amount":123,"amount_string":"1230 Points","payout_format":"{payout*10:,.0f} Points","redeemable_amount":100,"redeemable_amount_string":"1000 Points"}},"UserWalletBalanceResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"wallet":{"$ref":"#/components/schemas/UserWalletBalance"}},"type":"object","required":["status","wallet"],"title":"UserWalletBalanceResponse"},"UserWalletConfig":{"properties":{"enabled":{"type":"boolean","title":"Enabled","description":"If enabled, the users' wallets are managed.","default":false},"amt":{"type":"boolean","title":"Amt","description":"Uses Amazon Mechanical Turk","default":false},"supported_payout_types":{"items":{"$ref":"#/components/schemas/PayoutType"},"type":"array","uniqueItems":true,"title":"Supported Payout Types"},"min_cashout":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Min Cashout","description":"Minimum cashout amount. If enabled is True and no min_cashout is set, will default to $0.01.","examples":["10.00"]}},"type":"object","title":"UserWalletConfig","description":"Stores configuration for the user wallet handling"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WXETOfferwall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/WXETOfferwallBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"WXETOfferwall","description":"Returns buckets from WXET as single tasks\nOfferwall code: `55a4e1a9`"},"WXETOfferwallBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"duration":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Duration","description":"The bucket's expected duration, in seconds"},"min_payout":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Min Payout","description":"The bucket's min payout, in usd cents"}},"additionalProperties":false,"type":"object","required":["id","uri","duration","min_payout"],"title":"WXETOfferwallBucket"},"WXETOfferwallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/WXETOfferwall"}},"type":"object","required":["info","offerwall"],"title":"WXETOfferwallResponse"},"WallAdjustedStatus":{"type":"string","enum":["ac","af","ca","cc"],"title":"WallAdjustedStatus"},"WallOut":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"source":{"$ref":"#/components/schemas/Source"},"buyer_id":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Buyer Id"},"req_survey_id":{"type":"string","maxLength":32,"title":"Req Survey Id"},"started":{"type":"string","format":"date-time","title":"Started"},"survey_id":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Survey Id"},"finished":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Finished"},"status":{"anyOf":[{"$ref":"#/components/schemas/Status"},{"type":"null"}]},"status_code_1":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"__High level status code for outcome of the session.__\nThis should only be NULL if the Status is ABANDON or TIMEOUT\n\nAllowed values: \n - __BUYER_FAIL__: User terminated in buyer survey\n - __BUYER_QUALITY_FAIL__: User terminated in buyer survey for quality reasons\n - __PS_FAIL__: User failed in marketplace's prescreener\n - __PS_QUALITY__: User rejected by marketplace for quality reasons\n - __PS_BLOCKED__: User is explicitly blocked by the marketplace. Note: on some marketplaces, users can have multiple PS_QUALITY terminations and still complete surveys.\n - __PS_OVERQUOTA__: User rejected by marketplace for over quota\n - __PS_DUPLICATE__: User rejected by marketplace for duplicate\n - __GRS_FAIL__: The user failed within the GRS Platform\n - __GRS_QUALITY_FAIL__: The user failed within the GRS Platform for quality reasons\n - __GRS_ABANDON__: The user abandoned/timed out within the GRS Platform\n - __PS_ABANDON__: The user abandoned/timed out within the marketplace's pre-screen system. Note: On most marketplaces, we have no way of distinguishing between this and BUYER_ABANDON. BUYER_ABANDON is used as the default, unless we know it is PS_ABANDON.\n - __BUYER_ABANDON__: The user abandoned/timed out within the client survey\n - __UNKNOWN__: The status code is not documented\n - __COMPLETE__: The user completed the task successfully\n - __MARKETPLACE_FAIL__: Something was wrong upon the user redirecting from the marketplace, e.g. no postback received, or url hashing failures.\n - __SESSION_START_FAIL__: User failed before being sent into a marketplace\n - __SESSION_CONTINUE_FAIL__: User failed between attempts\n - __SESSION_START_QUALITY_FAIL__: User failed before being sent into a marketplace for \"security\" reasons\n - __SESSION_CONTINUE_QUALITY_FAIL__: User failed between attempts for \"security\" reasons","examples":["COMPLETE"]},"status_code_2":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"This should be set if the Wall.status_code_1 is MARKETPLACE_FAIL\n\nAllowed values: \n - __URL_HASHING_CHECK_FAILED__: The redirect URL (coming back from the marketplace) failed hashing checks\n - __BROKEN_REDIRECT__: The redirect URL was missing required query params or was unparseable\n - __INVALID_REDIRECT__: The redirect URL was invalid or inconsistent in some way and as a result we could not determine the outcome. This could be if a redirect received did not match the user's most recent attempt.\n - __INVALID_MARKETPLACE_POSTBACK__: The redirect indicated a complete, but no/invalid Postback was received from the marketplace\n - __NO_MARKETPLACE_POSTBACK__: No/invalid Postback was received from the marketplace. Used in cases where the redirect does not contain a status.\n - __COMPLETE_TOO_FAST__: The marketplace indicates the user completed the survey, but we don't think this is valid due to speeding. Generally this cutoff is the 95th percentile of our calculated CompletionTime survey stat.\n - __INTERNAL_ERROR__: Something happened during the handling of this redirect (on our side)","examples":[null]},"ext_status_code_1":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Ext Status Code 1"},"ext_status_code_2":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Ext Status Code 2"},"ext_status_code_3":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Ext Status Code 3"},"report_value":{"anyOf":[{"$ref":"#/components/schemas/ReportValue"},{"type":"null"}]},"report_notes":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Report Notes"},"adjusted_status":{"anyOf":[{"$ref":"#/components/schemas/WallAdjustedStatus"},{"type":"null"}]},"adjusted_timestamp":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Adjusted Timestamp"},"user_cpi":{"type":"integer","title":"User Cpi","description":"\n The amount the user would earn from completing this task, if the status was a complete.\n If the BP has no payout xform, the user_cpi is None. This is analogous to the session's\n user_payout.\n ","examples":[123]},"user_cpi_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Cpi String","description":"If a payout transformation is configured on this account, this is the amount to display to the user","examples":["123 Points"]}},"additionalProperties":false,"type":"object","required":["source","req_survey_id"],"title":"WallOut"},"WallStatusCode2":{"type":"integer","enum":[12,16,17,13,18,14,15],"title":"WallStatusCode2","description":"This should be set if the Wall.status_code_1 is MARKETPLACE_FAIL"},"YieldManConfig":{"properties":{"category_request":{"$ref":"#/components/schemas/OfferWallCategoryRequest"},"scoring_params":{"$ref":"#/components/schemas/OfferWallRequestYieldmanParams"}},"type":"object","title":"YieldManConfig"}}},"tags":[{"name":"Offerwall","description":"Generate and retrieve various offerwall groupings. These results should be called \"on the \n fly\" just before presenting the results to a respondent. Results greater than 3 minutes old should not be \n presented to the respondent. When a respondent takes any action on a generated offerwall (they go into a \n bucket), the offerwall should be discarded. Any subsequent bucket entrances will fail. The following reserved \n are reserved keywords. However, any additional query parameter that is provided, will be returned back in the \n TaskResponse response in the kwargs attribute.\n\n {'b', 'bp_user_id', 'bpuid', 'country_codes', 'country', 'cpi', 'currency', \n 'description', 'duration', 'entry_link', 'i', 'idx', 'info', 'ip', 'kwargs',\n 'l', 'lang_codes', 'languages', 'loi', 'name', 'payout', 'product_id', \n 'quality', 'quality_category', 'req_loi', 'status', 'task_uri', 'tasks', \n 'tsid', 'uri', 'url', 'country_iso', 'format', 'behavior'}"},{"name":"Status","description":"Retrieving the status of respondent attempts, either by brokerage account filtering, or a specific Task Status ID (TSID) that is provided in the redirect."}]}
\ No newline at end of file +{"openapi":"3.1.0","info":{"title":"General Research Full Service Brokerage API","version":"1.2.0"},"servers":[{"url":"https://fsb.generalresearch.com/","description":"Production environment"}],"paths":{"/{product_id}/offerwall/b145b803/":{"get":{"tags":["Offerwall"],"summary":"Topn Plus Offerwall","description":"Same as the TopNOfferWall, but the buckets include contents.","operationId":"TopN_Plus_Offerwall__product_id__offerwall_b145b803__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopNPlusOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/d48cce47/":{"get":{"tags":["Offerwall"],"summary":"Topn Plus Block Offerwall","description":"Same as the TopNOfferWall, but the buckets include contents and no\nbuckets are returned if the user is blocked.","operationId":"TopN_Plus_Block_Offerwall__product_id__offerwall_d48cce47__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopNPlusBlockOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/1e5f0af8/":{"get":{"tags":["Offerwall"],"summary":"Topn Plus Block Recontact Offerwall","description":"Same as the TopNOfferWall, but the buckets include contents, no buckets\nare returned if the user is blocked, and each bucket includes a\n`is_recontact` key.","operationId":"TopN_Plus_Block_Recontact_Offerwall__product_id__offerwall_1e5f0af8__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopNPlusBlockRecontactOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/5481f322/":{"get":{"tags":["Offerwall"],"summary":"Starwall Plus Offerwall","description":"Same as the StarwallOfferWall, but the buckets include contents.","operationId":"Starwall_Plus_Offerwall__product_id__offerwall_5481f322__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StarwallPlusOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/7fa1b3f4/":{"get":{"tags":["Offerwall"],"summary":"Starwall Plus Block Offerwall","description":"Same as the StarwallOfferWall, but the buckets include contents and no\nbuckets are returned if the user is blocked.","operationId":"Starwall_Plus_Block_Offerwall__product_id__offerwall_7fa1b3f4__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StarwallPlusBlockOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/630db2a4/":{"get":{"tags":["Offerwall"],"summary":"Starwall Plus Block Recontact Offerwall","description":"Same as the StarwallOfferWall, but the buckets include contents, no buckets\nare returned if the user is blocked, and each bucket includes a\n`is_recontact` key.","operationId":"Starwall_Plus_Block_Recontact_Offerwall__product_id__offerwall_630db2a4__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":3,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"split_by","in":"query","required":false,"schema":{"enum":["payout","duration"],"type":"string","description":"Cluster tasks by payout or duration","default":"payout","title":"Split By"},"description":"Cluster tasks by payout or duration"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StarwallPlusBlockRecontactOfferWallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/5fa23085/":{"get":{"tags":["Offerwall"],"summary":"Marketplace Offerwall","description":"Returns buckets grouped by marketplace, one per marketplace, with the\ntasks ordered by quality.","operationId":"Marketplace_Offerwall__product_id__offerwall_5fa23085__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":3,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketplaceOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/6f27b1ae/":{"get":{"tags":["Offerwall"],"summary":"One Shot Offerwall","description":"Each bucket has only 1 single task, and only basic info is returned\n about each bucket.","operationId":"One_Shot_Offerwall__product_id__offerwall_6f27b1ae__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":8,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"sources","in":"query","required":false,"schema":{"type":"array","uniqueItems":true,"items":{"$ref":"#/components/schemas/Source"},"description":"Restrict tasks to those from these marketplaces only.","title":"Sources"},"description":"Restrict tasks to those from these marketplaces only."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OneShotOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/55a4e1a9/":{"get":{"tags":["Offerwall"],"summary":"Wxet Offerwall","description":"Each bucket has only 1 single task, and only basic info is returned\n about each bucket.","operationId":"WXET_Offerwall__product_id__offerwall_55a4e1a9__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":8,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WXETOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/37d1da64/":{"get":{"tags":["Offerwall"],"summary":"Offerwall Softpair","description":"This offerwall contains tasks for which the user has a conditional\neligibility. The questions that a user must answer to determine the\neligibility are included within each bucket. Additionally, the question\ndefinitions are included for convenience.","operationId":"offerwall_softpair__product_id__offerwall_37d1da64__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":12,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":1,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"max_options","in":"query","required":false,"schema":{"type":"integer","description":"Max number of options an allowed question can have (allowed to be asked)","default":40,"title":"Max Options"},"description":"Max number of options an allowed question can have (allowed to be asked)"},{"name":"max_questions","in":"query","required":false,"schema":{"type":"integer","description":"Max number of missing questions on a single bin","default":3,"title":"Max Questions"},"description":"Max number of missing questions on a single bin"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SoftPairOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/37d1da64/{offerwall_id}/":{"post":{"tags":["Offerwall"],"summary":"Offerwall Softpair Post","description":"This is a custom endpoint for a softpair-style offerwall that allows\nyou to update profiling answers and then immediately get back the\nupdated offerwall","operationId":"offerwall_softpair_post__product_id__offerwall_37d1da64__offerwall_id___post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"offerwall_id","in":"path","required":true,"schema":{"type":"string","description":"The offerwall ID the user is interacting with","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"],"title":"Offerwall Id"},"description":"The offerwall ID the user is interacting with"},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"session_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"A session id (uuid4.hex)","examples":["dd6aa32cfe834839a80195563f528337"],"title":"Session Id"},"description":"A session id (uuid4.hex)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_offerwall_softpair_post__product_id__offerwall_37d1da64__offerwall_id___post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SoftPairOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/7a89dcdb/":{"get":{"tags":["Offerwall"],"summary":"Offerwall Softpair Block","description":"This offerwall contains tasks for which the user has a conditional\neligibility. The questions that a user must answer to determine the\neligibility are included within each bucket. Additionally, the question\ndefinitions are included for convenience.\nNo buckets are returned if the user is blocked.","operationId":"offerwall_softpair_block__product_id__offerwall_7a89dcdb__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"languages","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)","title":"Languages"},"description":"Respondent's desired languages (ISO 639-2/B, lowercase)"},{"name":"behavior","in":"query","required":false,"schema":{"type":"string","maxLength":12,"description":"Allows using custom scoring functions. Please discuss directly with GRL.","title":"Behavior"},"description":"Allows using custom scoring functions. Please discuss directly with GRL."},{"name":"min_payout","in":"query","required":false,"schema":{"type":"string","description":"Decimal representation of the minimum amount of USD that any of the tasks will pay","examples":["1.23"],"title":"Min Payout"},"description":"Decimal representation of the minimum amount of USD that any of the tasks will pay"},{"name":"duration","in":"query","required":false,"schema":{"type":"integer","description":"Maximum length of desired task (in seconds).","title":"Duration"},"description":"Maximum length of desired task (in seconds)."},{"name":"n_bins","in":"query","required":false,"schema":{"type":"integer","description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split.","default":12,"title":"N Bins"},"description":"Number of bins requested in the offerwall. If n_bins=1 there is no grouping and you get all \n duration + payout metrics on across all (as shown by the availability_count).\n n_bins=0 will return back an empty bucket array, however, the availability_count will still show \n the total opportunities available for that bpuid + passed in parameter settings.\n n_bins=∞ (or any positive integer) will return back the total number of bins capable of being uniquely split."},{"name":"min_bin_size","in":"query","required":false,"schema":{"type":"integer","description":"Minimum number of tasks that need to be in a bucket","default":1,"title":"Min Bin Size"},"description":"Minimum number of tasks that need to be in a bucket"},{"name":"max_options","in":"query","required":false,"schema":{"type":"integer","description":"Max number of options an allowed question can have (allowed to be asked)","default":40,"title":"Max Options"},"description":"Max number of options an allowed question can have (allowed to be asked)"},{"name":"max_questions","in":"query","required":false,"schema":{"type":"integer","description":"Max number of missing questions on a single bin","default":3,"title":"Max Questions"},"description":"Max number of missing questions on a single bin"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SoftPairOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/offerwall/7a89dcdb/{offerwall_id}/":{"post":{"tags":["Offerwall"],"summary":"Offerwall Softpair Block Post","description":"This is a custom endpoint for a softpair-style offerwall that allows\nyou to update profiling answers and then immediately get back the\nupdated offerwall","operationId":"offerwall_softpair_block_post__product_id__offerwall_7a89dcdb__offerwall_id___post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"offerwall_id","in":"path","required":true,"schema":{"type":"string","description":"The offerwall ID the user is interacting with","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"],"title":"Offerwall Id"},"description":"The offerwall ID the user is interacting with"},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"session_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"A session id (uuid4.hex)","examples":["dd6aa32cfe834839a80195563f528337"],"title":"Session Id"},"description":"A session id (uuid4.hex)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_offerwall_softpair_block_post__product_id__offerwall_7a89dcdb__offerwall_id___post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SoftPairOfferwallResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/status/{tsid}/":{"get":{"tags":["Status"],"summary":"Get Task Status","description":"Retrieve the status of a session by a specific Task Status ID (`tsid`)\nthat is provided in the redirect.\n\nGiven a `tsid`, get back the status of the wall event. This endpoint\nconflates the idea of a wall event and a session, but for these\npurposes, the status and payout of the last wall event in a session\nequals the status and payout for the session.","operationId":"Get_Task_Status__product_id__status__tsid___get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"tsid","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Task Status ID","description":"A unique identifier for the session","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"description":"A unique identifier for the session"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskStatusResponse"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusInfoResponseFail"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusInfoResponseFail"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusInfoResponseFail"}}},"description":"Internal Server Error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/status/":{"get":{"tags":["Status"],"summary":"List Task Statuses","operationId":"List_Task_Statuses__product_id__status__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":false,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Filters the returned task status to only include those done by this user.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Filters the returned task status to only include those done by this user."},{"name":"started_after","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","format":"date-time"}],"description":"Retrieve sessions which were started after this timestamp. If `NULL`, default is 10 days ago.","examples":["2026-09-08T22:12:19.351267Z",1788819139],"title":"Started After"},"description":"Retrieve sessions which were started after this timestamp. If `NULL`, default is 10 days ago."},{"name":"started_before","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","format":"date-time"}],"description":"Retrieve sessions which were started before this timestamp. If `NULL`, default is now.","title":"Started Before"},"description":"Retrieve sessions which were started before this timestamp. If `NULL`, default is now."},{"name":"status","in":"query","required":false,"schema":{"type":"integer","description":"Filter for sessions with this status. See Task Status for description of status.","examples":[3],"title":"Status"},"description":"Filter for sessions with this status. See Task Status for description of status."},{"name":"reconciled","in":"query","required":false,"schema":{"type":"boolean","description":"Only retrieve sessions that have been adjusted. If `reconciled` is True, `adjusted_after` is required.","default":false,"title":"Reconciled"},"description":"Only retrieve sessions that have been adjusted. If `reconciled` is True, `adjusted_after` is required."},{"name":"adjusted_after","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","format":"date-time"}],"description":"Retrieve sessions which were adjusted after this timestamp.","title":"Adjusted After"},"description":"Retrieve sessions which were adjusted after this timestamp."},{"name":"adjusted_before","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","format":"date-time"}],"description":"Retrieve sessions which were adjusted before this timestamp. Default is now.","title":"Adjusted Before"},"description":"Retrieve sessions which were adjusted before this timestamp. Default is now."},{"name":"order_by","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated fields to order by","examples":["-started","started"],"default":"-started","title":"Order By"},"description":"Comma-separated fields to order by"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TasksStatusResponse"}}}},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponseFailure"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponseFailure"}}},"description":"Unauthorized"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponseFailure"}}},"description":"Internal Server Error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/report/":{"post":{"tags":["Report"],"summary":"Report Task","description":"Send a \"Task Report\" for a respondent's latest started survey,\nregardless of the survey's outcome. Task Reports cannot be provided for\ntask attempts by tsid, or through any other method, they must be given\nimmediately after the respondent's task attempt.\n\nIf a latest attempt can't be found, a ReportTaskResponse is still returned.\n\nIf multiple reasons want to be provided, they should be done at the\nsame time. If multiple reports are given for the same latest started\ntask, the note attribute will be overwritten, while the new unique\nreasons will be appended to the task attempt.\n\nTask Report system contributions by client applications are immediately\nleveraged to aid yield management strategies. For those reasons, it's\nimportant that the respondent submits a Task Report as soon as possible\nand applications doesn't offload this request for a later time. Task\nReport timestamps are a critical component of understanding a Task\nexperience for a respondent.\n\nFor the BP to tell us about a bad survey they want to report\nThe POSTed data looks like:\n {\n 'bpuid': '1234',\n 'reasons': [2],\n 'notes': \"This survey was the worst!\"\n }\nReturns {'status': 'success', 'msg': error_msg}","operationId":"report_task__product_id__report__post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportTask"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/profiling-questions/":{"get":{"tags":["Profiling Questions"],"summary":"Get Profiling Questions","description":"Gets a list of profiling questions to ask this user. These questions\nare, as of right now, unanswered. Once a question is answered, it\n(and any other question whose answer is inferred from other answers) is\nremoved from the list. The questions returned are ordered by\nimportance (the key task_score) in descending order.","operationId":"get_profiling_questions__product_id__profiling_questions__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"ip","in":"query","required":false,"schema":{"type":"string","description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided.","title":"Ip"},"description":"Respondent's IP address (IPv4 or IPv6). Either 'ip' must be provided, or 'country_iso' must be provided if 'ip' is not provided."},{"name":"country_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"language_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{3}$","description":"Respondent's desired language (ISO 639-2/B, lowercase)","examples":["eng"],"title":"Language Iso"},"description":"Respondent's desired language (ISO 639-2/B, lowercase)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","exclusiveMinimum":0,"description":"Number of questions to return. Questions are ordered, so, selects the N most important. If no limit is passed, then the number of questions returned is dynamically calculated by the questions' importance.","title":"Limit"},"description":"Number of questions to return. Questions are ordered, so, selects the N most important. If no limit is passed, then the number of questions returned is dynamically calculated by the questions' importance."},{"name":"is_grs","in":"query","required":false,"schema":{"type":"boolean","description":"If it's the GRS system.","default":false,"title":"Is Grs"},"description":"If it's the GRS system."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpkQuestionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Profiling Questions"],"summary":"Submit Profiling Questions","description":"Send the answers to one or more of these questions for a user. A\nquestion is uniquely specified by the question_id key. The answer is:\nthe choice_id if the question_type is \"MC\" the actual entered text if\nthe question_type is \"TE\"","operationId":"submit_profiling_questions__product_id__profiling_questions__post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"session_id","in":"query","required":false,"schema":{"type":"string","description":"The unique identifier for a session in which these questions were asked. Must be a valid UUID. Optional.","title":"Session Id"},"description":"The unique identifier for a session in which these questions were asked. Must be a valid UUID. Optional."},{"name":"async","in":"query","required":false,"schema":{"type":"boolean","description":"For internal use.","default":true,"title":"Async"},"description":"For internal use."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_submit_profiling_questions__product_id__profiling_questions__post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/profiling-info/":{"get":{"tags":["Profiling Questions"],"summary":"Profiling Info","description":"Get UPK Ontology (list of properties, property info, and allowed values)","operationId":"profiling_info__product_id__profiling_info__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"country_iso","in":"query","required":true,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"language_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{3}$","description":"Respondent's desired language (ISO 639-2/B, lowercase)","examples":["eng"],"title":"Language Iso"},"description":"Respondent's desired language (ISO 639-2/B, lowercase)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfilingInfoResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/user-profile/":{"get":{"tags":["Profiling Questions"],"summary":"User Profile","operationId":"user_profile__product_id__user_profile__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"country_iso","in":"query","required":true,"schema":{"type":"string","pattern":"^[a-z]{2}$","description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)","examples":["us"],"title":"Country Iso"},"description":"Respondent's country code (ISO 3166-1 alpha-2, lowercase)"},{"name":"language_iso","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-z]{3}$","description":"Respondent's desired language (ISO 639-2/B, lowercase)","examples":["eng"],"title":"Language Iso"},"description":"Respondent's desired language (ISO 639-2/B, lowercase)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfoResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/user/{product_user_id}/profile/":{"get":{"tags":["Product User"],"summary":"User Profile","operationId":"user_profile__product_id__user__product_user_id__profile__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfileResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Product User"],"summary":"User Profile Update","operationId":"user_profile_update__product_id__user__product_user_id__profile__patch","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfileUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/user/by-marketplace-pid/":{"get":{"tags":["Product User"],"summary":"User Profile Filter By Marketplace Pid","operationId":"user_profile_filter_by_marketplace_pid__product_id__user_by_marketplace_pid__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"source","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Source","description":"The marketplace to search"},"description":"The marketplace to search"},{"name":"pid","in":"query","required":true,"schema":{"type":"array","items":{"type":"string","minLength":32,"maxLength":32},"description":"The user's marketplace PID. Supports multiple params","title":"Pid"},"description":"The user's marketplace PID. Supports multiple params"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfilesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/leaderboard/timespan/{board_code}/":{"get":{"tags":["Leaderboard"],"summary":"Timespan Leaderboard","operationId":"Timespan_Leaderboard__product_id__leaderboard_timespan__board_code___get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"board_code","in":"path","required":true,"schema":{"$ref":"#/components/schemas/LeaderboardCode","description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts"},"description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts"},{"name":"freq","in":"query","required":true,"schema":{"$ref":"#/components/schemas/LeaderboardFrequency","description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00"},"description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00"},{"name":"country_iso","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^[a-z]{2}$"},{"type":"null"}],"description":"Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request.","examples":["us"],"title":"Country Iso"},"description":"Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request."},{"name":"bpuid","in":"query","required":false,"schema":{"type":"string","minLength":3,"maxLength":128,"description":"Brokerage Product User ID. If passed, this results in the filtering of rows to include this user's row, plus rows above and below in rank (up to `limit` rows). Otherwise, the top `limit` rows are returned.","examples":["app-user-9329ebd"],"title":"Bpuid"},"description":"Brokerage Product User ID. If passed, this results in the filtering of rows to include this user's row, plus rows above and below in rank (up to `limit` rows). Otherwise, the top `limit` rows are returned."},{"name":"within_time","in":"query","required":false,"schema":{"type":"string","format":"date-time","description":"Any local timestamp falling within the desired leaderboard's period. If not passed, uses the country's current time period.","examples":["2024-02-27","2024-02-27T15:30:00"],"title":"Within Time"},"description":"Any local timestamp falling within the desired leaderboard's period. If not passed, uses the country's current time period."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","exclusiveMaximum":5000,"exclusiveMinimum":0,"description":"Max number of rows to return","default":50,"title":"Limit"},"description":"Max number of rows to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/leaderboard/timespan/{board_code}/winners/":{"get":{"tags":["Leaderboard"],"summary":"Leaderboard Winners","description":"Get the Winners of leaderboard contests","operationId":"Leaderboard_Winners__product_id__leaderboard_timespan__board_code__winners__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"board_code","in":"path","required":true,"schema":{"$ref":"#/components/schemas/LeaderboardCode","description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts"},"description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts"},{"name":"freq","in":"query","required":true,"schema":{"$ref":"#/components/schemas/LeaderboardFrequency","description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00"},"description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00"},{"name":"country_iso","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^[a-z]{2}$"},{"type":"null"}],"description":"Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request.","examples":["us"],"title":"Country Iso"},"description":"Country ISO Code (ISO 3166-1 alpha-2, lowercase) of the requested leaderboard. If not passed, thecountry is determined from the IP address of the HTTP request."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardWinnerResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/contest/{contest_uuid}/user/{product_user_id}/":{"get":{"tags":["Contest"],"summary":"Get Contest User View","description":"Gets a Contest. Includes several keys that are specific to the requesting user:\n e.g. user_winnings, user_amount.","operationId":"Get_Contest_User_View__product_id__contest__contest_uuid__user__product_user_id___get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"contest_uuid","in":"path","required":true,"schema":{"type":"string","title":"Contest Uuid"}},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RaffleUserView"},{"$ref":"#/components/schemas/LeaderboardContestUserView"},{"$ref":"#/components/schemas/MilestoneUserView"}],"title":"Response Get Contest User View Product Id Contest Contest Uuid User Product User Id Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/contest/user/{product_user_id}/entered/":{"get":{"tags":["Contest"],"summary":"List Contests User Entered","description":"Get contests the requesting user has entered. This includes contests such as milestone\n contests where the user was automatically entered by having a complete for instance.","operationId":"List_Contests_User_Entered__product_id__contest_user__product_user_id__entered__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/RaffleUserView"},{"$ref":"#/components/schemas/LeaderboardContestUserView"},{"$ref":"#/components/schemas/MilestoneUserView"}]},"title":"Response List Contests User Entered Product Id Contest User Product User Id Entered Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/contest/user/{product_user_id}/eligible/":{"get":{"tags":["Contest"],"summary":"List Contests User Eligible","description":"Get contests the requesting user is eligible for.\nContest eligibility may involve the user's country, which is determined by the ip address\n of THIS request.","operationId":"List_Contests_User_Eligible__product_id__contest_user__product_user_id__eligible__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/RaffleUserView"},{"$ref":"#/components/schemas/LeaderboardContestUserView"},{"$ref":"#/components/schemas/MilestoneUserView"}]},"title":"Response List Contests User Eligible Product Id Contest User Product User Id Eligible Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/contest/user/{product_user_id}/winner/":{"get":{"tags":["Contest"],"summary":"List Contests User Winner","description":"Get contests the requesting user has won a prize for.","operationId":"List_Contests_User_Winner__product_id__contest_user__product_user_id__winner__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"product_user_id","in":"path","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/RaffleUserView"},{"$ref":"#/components/schemas/LeaderboardContestUserView"},{"$ref":"#/components/schemas/MilestoneUserView"}]},"title":"Response List Contests User Winner Product Id Contest User Product User Id Winner Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/wallet/":{"get":{"tags":["Wallet"],"summary":"Get User Wallet Balance","description":"Retrieve the current balance of a respondent's wallet balance for display","operationId":"get_user_wallet_balance__product_id__wallet__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserWalletBalanceResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/wallets/":{"get":{"tags":["Wallet"],"summary":"Get User Wallets","description":"Retrieve a respondent's wallets and balances for display","operationId":"get_user_wallets__product_id__wallets__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLedgerWallets"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/cashout_methods/":{"get":{"tags":["Wallet"],"summary":"Get Cashout Methods","description":"Retrieve the current available mechanisms that a user can use to retrieve funds.","operationId":"get_cashout_methods__product_id__cashout_methods__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CashoutMethodsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Wallet"],"summary":"Create Cashout Method","description":"A user can only have 1 unique Cashout Method of any type at a time (eg. they can't create multiple paypal\npayout methods). Cashout Methods cannot be edited, they can only be created or deleted. Creating different\nCashout Methods will require type-dependent request data attributes in order to be successfully created.\n\nCurrently only creating paypal or Cash in Mail cashout methods are supported","operationId":"create_cashout_method__product_id__cashout_methods__post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CreatePayPalCashoutMethodRequest"},{"$ref":"#/components/schemas/CreateCashMailCashoutMethodRequest"}],"discriminator":{"propertyName":"type","mapping":{"PAYPAL":"#/components/schemas/CreatePayPalCashoutMethodRequest","CASH_IN_MAIL":"#/components/schemas/CreateCashMailCashoutMethodRequest"}},"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CashoutMethodResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/cashout_methods/{cashout_method_id}/":{"delete":{"tags":["Wallet"],"summary":"Delete Cashout Method","description":"Delete a cashout method","operationId":"delete_cashout_method__product_id__cashout_methods__cashout_method_id___delete","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"cashout_method_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"description":"The ID of the cashout method to delete","title":"Cashout Method Id"},"description":"The ID of the cashout method to delete"},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/cashout/":{"post":{"tags":["Wallet"],"summary":"Submit User Cashout","description":"Request a cashout taken from a user's available wallet balance, to be paid via a particular\ncashout method.","operationId":"submit_user_cashout__product_id__cashout__post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCashoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CashoutRequestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/cashout/{cashout_id}/":{"get":{"tags":["Wallet"],"summary":"Get Cashout Detail","description":"Retrieve the details for a specific Cashout event","operationId":"get_cashout_detail__product_id__cashout__cashout_id___get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"cashout_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"description":"The cashout event ID","title":"Cashout Id"},"description":"The cashout event ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CashoutRequestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/transaction_history/":{"get":{"tags":["Wallet"],"summary":"Get User Transaction History","description":"Return array of user events that altered their wallet balance","operationId":"get_user_transaction_history__product_id__transaction_history__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"bpuid","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":128,"title":"Brokerage Product User ID","description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values.","examples":["app-user-9329ebd"]},"description":"A unique identifier for each user, which is set by the Supplier. Must be consistent across respondent entrances and unique to the platform. It should not contain any sensitive information like email or names, and should avoid using any incrementing values."},{"name":"created_after","in":"query","required":false,"schema":{"type":"string","format":"date-time","description":"Filter transactions created after this timestamp","examples":["2025-01-01T04:20:00.000000Z"],"title":"Created After"},"description":"Filter transactions created after this timestamp"},{"name":"created_before","in":"query","required":false,"schema":{"type":"string","format":"date-time","description":"Filter transactions created before this timestamp","examples":["2026-09-08T22:12:19.380546Z"],"title":"Created Before"},"description":"Filter transactions created before this timestamp"},{"name":"order_by","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated fields to order by","examples":["-created","created"],"default":"created","title":"Order By"},"description":"Comma-separated fields to order by"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLedgerTransactionsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/cashout_method/{cashout_method_id}/value/":{"get":{"tags":["Wallet"],"summary":"Get Expected Redemption Value","description":"Use this function to get the \"value\" (in local currency, e.g. CAD, BTC) for this\n cashout_method, for amount (in USD cents).","operationId":"get_expected_redemption_value__product_id__cashout_method__cashout_method_id__value__get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"cashout_method_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"description":"The ID of the cashout method","examples":["0d363971792249b1a34586f092819e4b"],"title":"Cashout Method Id"},"description":"The ID of the cashout method"},{"name":"amount","in":"query","required":true,"schema":{"type":"integer","description":"(USD cents) The amount to convert","examples":[100],"title":"Amount"},"description":"(USD cents) The amount to convert"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CashoutMethodForeignValueResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/wallet/attempt-credit/":{"post":{"tags":["Wallet"],"summary":"Claim Latest Attempt Credit","description":"Attempt to claim an Attempt Credit from the user's latest Session.","operationId":"claim_latest_attempt_credit__product_id__wallet_attempt_credit__post","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttemptCreditClaimBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttemptCreditClaimResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{product_id}/wallet/attempt-credit/{tsid}/":{"get":{"tags":["Wallet"],"summary":"Get Attempt Credit","description":"Get an Attempt Credit by a user's Session.","operationId":"get_attempt_credit__product_id__wallet_attempt_credit__tsid___get","parameters":[{"name":"product_id","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Brokerage Product ID","description":"The Brokerage Product ID provided by GRL.","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"description":"The Brokerage Product ID provided by GRL."},{"name":"tsid","in":"path","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Task Status ID","description":"A unique identifier for the session","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"description":"A unique identifier for the session"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttemptCreditClaimResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thl/manage_cashout/":{"post":{"tags":["THL"],"summary":"Manage Cashout","operationId":"manage_cashout_thl_manage_cashout__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManageCashoutBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPayoutEvent"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/inventory/":{"get":{"tags":["Dashboard"],"summary":"Marketplace Inventory","operationId":"marketplace_inventory_dashboard_inventory__get","parameters":[{"name":"marketplace","in":"query","required":true,"schema":{"const":"WXET","type":"string","examples":["WXET"],"title":"Marketplace"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketplaceSummary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ws/debug/":{"get":{"tags":["Streams"],"summary":"Websocket Debug Page","operationId":"websocket_debug_page_ws_debug__get","parameters":[{"name":"product_id","in":"query","required":true,"schema":{"type":"string","minLength":32,"maxLength":32,"title":"Product Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ws/events/docs/":{"get":{"tags":["Streams"],"summary":"Websocket Events Docs","description":"WebSocket endpoint: `/ws/events/`\n\nProtocol:\n- Client connects\n- Client sends a SubscribeMessage `{ kind: \"subscribe\", product_id: \"{product_id}\" }`\n- Server sends a PingMessage `{ kind: \"ping\" }`\n- Client must reply a PongMessage: `{ kind: \"pong\" }`\n- Other messages are EventMessage or StatsMessage\n\nClose on missing pong after 20s.","operationId":"websocket_events_docs_ws_events_docs__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/EventMessage"},{"$ref":"#/components/schemas/StatsMessage"},{"$ref":"#/components/schemas/PingMessage"},{"$ref":"#/components/schemas/SubscribeMessage"},{"$ref":"#/components/schemas/PongMessage"}],"title":"Response Websocket Events Docs Ws Events Docs Get"}}}}}}}},"components":{"schemas":{"AccessType":{"type":"string","enum":["residential","mobile","satellite","datacenter","enterprise","unknown"],"title":"AccessType","description":"How the network is used"},"AccountType":{"type":"string","enum":["bp_commission","bp_wallet","user_wallet","user_attempt_credit","cash","revenue","expense","contest_wallet","credit_line","wa_wallet","wa_budget_pool","wa_held","wa_credit_line"],"title":"AccountType"},"AdjustmentType":{"properties":{"amount":{"type":"integer","title":"Amount","description":"The total amount (in USD cents) that the Brokerage Producthas earned within a respective time period from a specificSource of Tasks."},"adjustment":{"$ref":"#/components/schemas/SessionAdjustedStatus","description":" - `ac` = ADJUSTED_TO_COMPLETE\n - `af` = ADJUSTED_TO_FAIL\n - `pa` = PAYOUT_ADJUSTMENT","examples":["af"]}},"type":"object","required":["amount","adjustment"],"title":"AdjustmentType"},"AggregateBySource":{"properties":{"total":{"type":"integer","minimum":0.0,"title":"Total","default":0},"by_source":{"additionalProperties":{"type":"integer","minimum":0.0},"propertyNames":{"$ref":"#/components/schemas/Source"},"type":"object","title":"By Source"}},"type":"object","title":"AggregateBySource"},"AmtCashoutMethodData":{"properties":{"type":{"type":"string","const":"AMT","title":"Type","default":"AMT"}},"type":"object","title":"AmtCashoutMethodData"},"AttemptCreditClaimBody":{"properties":{"bpuid":{"type":"string","maxLength":128,"minLength":3,"title":"Bpuid","description":"(product_user_id) The user requesting the claim","examples":["app-user-9329ebd"]}},"type":"object","required":["bpuid"],"title":"AttemptCreditClaimBody"},"AttemptCreditClaimResponse":{"properties":{"amount":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amount","description":"(USD cents) The amount given to the user as an attempt credit."}},"type":"object","required":["amount"],"title":"AttemptCreditClaimResponse"},"AuditLog":{"properties":{"id":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Id"},"user_id":{"type":"integer","exclusiveMinimum":0.0,"title":"User Id"},"created":{"type":"string","format":"date-time","title":"Created","description":"When did this event occur","examples":["2026-09-08T22:12:17.882731Z"]},"level":{"$ref":"#/components/schemas/AuditLogLevel","description":"The level of importance for this event. Works the same as python logging levels. It is an integer 0 - 50, and implementers of this field could map it to the predefined levels: (`CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`).This is NOT the same concept as the 'strength' of whatever event happened; it is just for sorting, filtering and display purposes. For e.g. multiple level 20 events != the 'importance' of one level 40 event.","examples":[30]},"event_type":{"type":"string","maxLength":64,"title":"Event Type","examples":["entrance-limit"]},"event_msg":{"anyOf":[{"type":"string","maxLength":256,"minLength":3},{"type":"null"}],"title":"Event Msg","description":"The event message. Could be displayed on user's page"},"event_value":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Event Value","description":"Optionally store a numeric value associated with this event. For e.g. if we recalculate the user's normalized recon rate, and it is 'high', we could store an event like (event_type='recon-rate', event_msg='higher than allowed recon rate' event_value=0.42)","examples":[0.42]}},"type":"object","required":["user_id","level","event_type"],"title":"AuditLog","description":"Table / Model for logging \"actions\" taken by a user or \"events\" that\nare related to a User"},"AuditLogLevel":{"type":"integer","enum":[50,50,40,30,30,20,10,0],"title":"AuditLogLevel"},"Body_offerwall_softpair_block_post__product_id__offerwall_7a89dcdb__offerwall_id___post":{"properties":{"answers":{"items":{"$ref":"#/components/schemas/UserQuestionAnswerIn"},"type":"array","title":"Answers"}},"type":"object","required":["answers"],"title":"Body_offerwall_softpair_block_post__product_id__offerwall_7a89dcdb__offerwall_id___post"},"Body_offerwall_softpair_post__product_id__offerwall_37d1da64__offerwall_id___post":{"properties":{"answers":{"items":{"$ref":"#/components/schemas/UserQuestionAnswerIn"},"type":"array","title":"Answers"}},"type":"object","required":["answers"],"title":"Body_offerwall_softpair_post__product_id__offerwall_37d1da64__offerwall_id___post"},"Body_submit_profiling_questions__product_id__profiling_questions__post":{"properties":{"answers":{"items":{"$ref":"#/components/schemas/UserQuestionAnswerIn"},"type":"array","title":"Answers"}},"type":"object","required":["answers"],"title":"Body_submit_profiling_questions__product_id__profiling_questions__post"},"BrokerageProductPayoutEvent":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Payout Event Unique Identifier","examples":["9453cd076713426cb68d05591c7145aa"]},"debit_account_uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Debit Account Uuid","description":"The LedgerAccount.uuid that money is being requested from. Thie User or Brokerage Product is retrievable through the LedgerAccount.reference_uuid","examples":["18298cb1583846fbb06e4747b5310693"]},"cashout_method_uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Cashout Method Uuid","description":"References a row in the account_cashoutmethod table. This is the cashout method that was used to request this payout. (A cashout is the same thing as a payout)","examples":["a6dc1fc1bf934557b952f253dee12813"]},"created":{"type":"string","format":"date-time","title":"Created"},"amount":{"type":"integer","exclusiveMaximum":9.223372036854776e+18,"exclusiveMinimum":0.0,"title":"Amount","description":"The USDCent amount int. This cannot be 0 or negative","examples":[531]},"status":{"anyOf":[{"$ref":"#/components/schemas/PayoutStatus"},{"type":"null"}],"description":" - `PENDING` = PENDING\n - `APPROVED` = APPROVED\n - `REJECTED` = REJECTED\n - `CANCELLED` = CANCELLED\n - `FAILED` = FAILED\n - `COMPLETE` = COMPLETE","default":"PENDING","examples":["COMPLETE"]},"ext_ref_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ext Ref Id"},"payout_type":{"$ref":"#/components/schemas/PayoutType","description":" - `PAYPAL` = PAYPAL\n - `TANGO` = TANGO\n - `DWOLLA` = DWOLLA\n - `ACH` = ACH\n - `WIRE` = WIRE\n - `CASH_IN_MAIL` = CASH_IN_MAIL\n - `PRIZE` = PRIZE\n - `AMT` = AMT\n - `AMT_BONUS` = AMT_BONUS\n - `AMT_ASSIGNMENT` = AMT_HIT","examples":["ACH"]},"request_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Request Data","description":"Stores payout-type-specific information that is used to request this payout from the external provider."},"order_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"$ref":"#/components/schemas/CashMailOrderData"},{"type":"null"}],"title":"Order Data","description":"Stores payout-type-specific order information that is returned from the external payout provider."},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","description":"The Brokerage Product that was paid out","examples":["1108d053e4fa47c5b0dbdcd03a7981e7"]},"method":{"$ref":"#/components/schemas/PayoutType","description":" - `PAYPAL` = PAYPAL\n - `TANGO` = TANGO\n - `DWOLLA` = DWOLLA\n - `ACH` = ACH\n - `WIRE` = WIRE\n - `CASH_IN_MAIL` = CASH_IN_MAIL\n - `PRIZE` = PRIZE\n - `AMT` = AMT\n - `AMT_BONUS` = AMT_BONUS\n - `AMT_ASSIGNMENT` = AMT_HIT","readOnly":true,"examples":["ACH"]},"amount_usd":{"type":"integer","minimum":0.0,"title":"Amount Usd","readOnly":true,"examples":["$10,000.000"]},"amount_usd_str":{"type":"string","title":"Amount Usd Str","readOnly":true,"examples":["$10,000.000"]}},"type":"object","required":["debit_account_uuid","cashout_method_uuid","amount","payout_type","product_id","method","amount_usd","amount_usd_str"],"title":"BrokerageProductPayoutEvent","description":"The amount\n\n- created: When the Brokerage Product was paid out"},"BucketTask":{"properties":{"id":{"type":"string","maxLength":32,"minLength":1,"title":"Id","description":"The internal task id for this task within the marketplace","examples":["6ov9jz3"]},"id_code":{"type":"string","maxLength":35,"minLength":3,"pattern":"^[a-z]{1,2}\\:.*","title":"Id Code","description":"The namespaced task id for this task within the marketplace","examples":["o:6ov9jz3"]},"source":{"$ref":"#/components/schemas/Source","examples":["o"]},"loi":{"type":"integer","maximum":5400.0,"exclusiveMinimum":1.0,"title":"Loi","description":"expected loi in seconds","examples":[612]},"payout":{"type":"integer","exclusiveMinimum":1.0,"title":"Payout","description":"integer cents","examples":[123]}},"type":"object","required":["id","id_code","source","loi","payout"],"title":"BucketTask","description":"This represents one of the \"tasks\" within a bucket's ordered list of tasks."},"Cardinality":{"type":"string","enum":["*","?"],"title":"Cardinality"},"CashMailCashoutMethodData":{"properties":{"type":{"type":"string","const":"CASH_IN_MAIL","title":"Type","default":"CASH_IN_MAIL"},"delivery_address":{"$ref":"#/components/schemas/USDeliveryAddress","description":"Delivery address where payment should be sent"}},"type":"object","required":["delivery_address"],"title":"CashMailCashoutMethodData"},"CashMailOrderData":{"properties":{"type":{"type":"string","const":"CASH_IN_MAIL","title":"Type","default":"CASH_IN_MAIL"},"shipping_cost":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Shipping Cost","description":"(USD cents) The shipping cost. This amount get charged to the BP."},"tracking_number":{"anyOf":[{"type":"string","maxLength":50,"minLength":1},{"type":"null"}],"title":"Tracking Number"},"shipping_method":{"anyOf":[{"$ref":"#/components/schemas/ShippingMethod","maxLength":50,"minLength":1},{"type":"null"}],"description":"Standard, express, etc."},"carrier":{"anyOf":[{"$ref":"#/components/schemas/ShippingCarrier","maxLength":50,"minLength":1},{"type":"null"}],"description":"Name of the shipping company, e.g., USPS, FedEx, DHL"},"ship_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ship Date"},"estimated_delivery_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Estimated Delivery Date"},"delivery_status":{"anyOf":[{"$ref":"#/components/schemas/DeliveryStatus","maxLength":50,"minLength":1},{"type":"null"}],"description":"Current status of delivery, e.g., pending, in transit, delivered"},"last_updated":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Updated","description":"Timestamp of the last status update"}},"type":"object","required":["shipping_cost"],"title":"CashMailOrderData"},"CashoutMethodForeignValue":{"properties":{"value":{"type":"integer","minimum":0.0,"title":"Value","description":"Value of the redemption in the currency's smallest unit."},"currency":{"$ref":"#/components/schemas/RedemptionCurrency","description":"Supported Currencies for Foreign Redemptions\n\nAllowed values: \n - __USD__ *(USD)*: US Dollars. Smallest Unit: Cents.\n - __CAD__ *(CAD)*: Canadian Dollars. Smallest Unit: Cents.\n - __GBP__ *(GBP)*: British Pounds. Smallest Unit: Pence.\n - __EUR__ *(EUR)*: Euros. Smallest Unit: Cents.\n - __INR__ *(INR)*: Indian Rupees. Smallest Unit: Paise.\n - __AUD__ *(AUD)*: Australian Dollars. Smallest Unit: Cents.\n - __PLN__ *(PLN)*: Polish Zloty. Smallest Unit: Grosz.\n - __SEK__ *(SEK)*: Swedish Krona. Smallest Unit: Öre.\n - __SGD__ *(SGD)*: Singapore Dollars. Smallest Unit: Cents.\n - __MXN__ *(MXN)*: Mexican Pesos. Smallest Unit: Centavos."},"value_string":{"type":"string","title":"Value String","description":"A string representation of the value in the currency."}},"type":"object","required":["value","currency","value_string"],"title":"CashoutMethodForeignValue","description":"Shows the expected value of a redemption in a foreign currency.","example":{"currency":"CAD","value":"138","value_string":"$1.38 CAD"}},"CashoutMethodForeignValueResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"cashout_method_value":{"$ref":"#/components/schemas/CashoutMethodForeignValue"}},"type":"object","required":["status","cashout_method_value"],"title":"CashoutMethodForeignValueResponse"},"CashoutMethodOut":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique ID for this cashout method"},"currency":{"type":"string","const":"USD","title":"Currency","description":"The currency of the cashout. Only USD is supported.","default":"USD"},"original_currency":{"anyOf":[{"$ref":"#/components/schemas/Currency"},{"type":"null"}],"description":"The base currency of the money paid out. This is used for e.g. sending an Amazon UK gift card"},"data":{"oneOf":[{"$ref":"#/components/schemas/PaypalCashoutMethodData"},{"$ref":"#/components/schemas/TangoCashoutMethodData"},{"$ref":"#/components/schemas/CashMailCashoutMethodData"},{"$ref":"#/components/schemas/AmtCashoutMethodData"}],"title":"Data","discriminator":{"propertyName":"type","mapping":{"AMT":"#/components/schemas/AmtCashoutMethodData","CASH_IN_MAIL":"#/components/schemas/CashMailCashoutMethodData","PAYPAL":"#/components/schemas/PaypalCashoutMethodData","TANGO":"#/components/schemas/TangoCashoutMethodData"}}},"description":{"type":"string","title":"Description","description":"The description of the cashout method.","default":""},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url","description":"Link to an image to display"},"max_value":{"type":"integer","exclusiveMinimum":0.0,"title":"Max Value","description":"(In lowest unit of the original_currency), The maximum amount that can be cashed out in one transaction."},"min_value":{"type":"integer","minimum":0.0,"title":"Min Value","description":"(In lowest unit of the original_currency), The minimum amount that can be cashed out in one transaction."},"name":{"type":"string","title":"Name","description":"A descriptive name for the cashout method."},"type":{"$ref":"#/components/schemas/PayoutType","description":"The method in which the requested payout is delivered.\n\nAllowed values: \n - __PAYPAL__ *(PAYPAL)*: User is paid out to their personal PayPal email address\n - __TANGO__ *(TANGO)*: User is paid out via a Tango Gift Card\n - __DWOLLA__ *(DWOLLA)*: DWOLLA\n - __ACH__ *(ACH)*: A payment is made to a bank account using ACH\n - __WIRE__ *(WIRE)*: A payment is made to a bank account using ACH\n - __CASH_IN_MAIL__ *(CASH_IN_MAIL)*: A payment is made in cash and mailed to the user.\n - __PRIZE__ *(PRIZE)*: A payment is made as a prize with some monetary value\n - __AMT__ *(AMT)*: This is used to designate either AMT_BONUS or AMT_HIT\n - __AMT_BONUS__ *(AMT_BONUS)*: Amazon Mechanical Turk as a Bonus\n - __AMT_ASSIGNMENT__ *(AMT_HIT)*: Amazon Mechanical Turk for a HIT"},"ext_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ext Id","description":"An external ID. Can be shown to a user to disambiguate a user's possibly multiple methods"},"usd_exchange_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Usd Exchange Rate"},"max_value_usd":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Max Value Usd","description":"(In lowest unit of USD), The maximum amount that can be cashed out in one transaction."},"min_value_usd":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Min Value Usd","description":"(In lowest unit of USD), The minimum amount that can be cashed out in one transaction."},"product_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Product Id","examples":["4fe381fb7186416cb443a38fa66c6557"]},"product_user_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":3},{"type":"null"}],"title":"Product User Id","description":"A unique identifier for each user, which is set by the Supplier. It should not contain any sensitive informationlike email or names, and should avoid using anyincrementing values.","examples":["app-user-9329ebd"]}},"type":"object","required":["id","data","max_value","min_value","name","type"],"title":"CashoutMethodOut","example":{"bpid":"6a3ddfb747344bbc93efadf1c3a16e1a","currency":"USD","data":{"disclaimer":"...","terms":"..."},"description":"...","id":"941d489c3ce04eb39a0ddb7f8f75db74","image_url":"https://d30s7yzk2az89n.cloudfront.net/images/brands/b238587-1200w-326ppi.png","max_value":25000,"min_value":500,"name":"Visa® Prepaid Card USD","type":"TANGO"}},"CashoutMethodResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"cashout_method":{"$ref":"#/components/schemas/CashoutMethodOut"}},"type":"object","required":["status","cashout_method"],"title":"CashoutMethodResponse"},"CashoutMethodsResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"cashout_methods":{"items":{"$ref":"#/components/schemas/CashoutMethodOut"},"type":"array","title":"Cashout Methods"}},"type":"object","required":["status","cashout_methods"],"title":"CashoutMethodsResponse"},"CashoutRequestInfo":{"properties":{"id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Id","description":"Unique ID for this cashout. This may be NULL if the status is REJECTED or FAILED, which may happen if the request is invalid.","examples":["3ceb847aaf9f40f4bd15b2b5e083abf6"]},"description":{"type":"string","title":"Description","description":"This is the name of the cashout method.","examples":["Visa® Prepaid Card USD"]},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"status":{"anyOf":[{"$ref":"#/components/schemas/PayoutStatus"},{"type":"null"}],"description":" - `PENDING` = PENDING\n - `APPROVED` = APPROVED\n - `REJECTED` = REJECTED\n - `CANCELLED` = CANCELLED\n - `FAILED` = FAILED\n - `COMPLETE` = COMPLETE","default":"PENDING","examples":["PENDING"]},"transaction_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Transaction Info"}},"type":"object","required":["id","description"],"title":"CashoutRequestInfo","description":"See models.thl.payout: PayoutEvent. We've confused a CashOut and a\nPayout. This is used only in the API response."},"CashoutRequestResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"cashout":{"$ref":"#/components/schemas/CashoutRequestInfo"}},"type":"object","required":["status","cashout"],"title":"CashoutRequestResponse"},"Category":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid","examples":["c109fbcaddac48cf88d5957c088ad446"]},"adwords_vertical_id":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Adwords Vertical Id"},"label":{"type":"string","maxLength":255,"title":"Label","examples":["Hair Loss"]},"path":{"type":"string","pattern":"^\\/.*[^\\/]$","title":"Path","examples":["/Beauty & Fitness/Hair Care/Hair Loss"]},"parent_uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Parent Uuid","examples":["2a3e09d812ac41458c84def5b25e4364"]}},"type":"object","required":["uuid","label","path"],"title":"Category"},"CategoryAssociation":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"The category ID","examples":["c8642a1b86d9460cbe8f7e8ae6e56ee4"]},"label":{"type":"string","maxLength":255,"title":"Label","description":"The category label","examples":["People & Society"]},"adwords_id":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Adwords Id","examples":["14"]},"adwords_label":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Adwords Label","examples":["People & Society"]},"p":{"type":"number","maximum":1.0,"minimum":0.0,"title":"P","description":"The strength of the association of this bucketwith this category. Will sum to 1 within a bucket.","examples":[1.0]}},"type":"object","required":["id","label","p"],"title":"CategoryAssociation","description":"Used in an offerwall. Stores the association between a category\nand a bucket, with a score."},"ContestEndCondition":{"properties":{"target_entry_amount":{"anyOf":[{"anyOf":[{"type":"integer","minimum":0.0},{"type":"integer","exclusiveMinimum":0.0}],"ge":1},{"type":"null"}],"title":"Target Entry Amount","description":"The contest is over once this amount is reached. (sum of all entry amount)"},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At","description":"The contest is over at this time."}},"type":"object","title":"ContestEndCondition","description":"Defines the conditions to evaluate to determine when the contest\nis over. Multiple conditions can be set. The contest is over\nonce ANY conditions are met."},"ContestEndReason":{"type":"string","enum":["cancelled","target_entry_amount","ends_at","max_winners"],"title":"ContestEndReason","description":"Defines why a contest ended"},"ContestEntry":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"entry_type":{"$ref":"#/components/schemas/ContestEntryType"},"amount":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"integer"}],"title":"Amount","description":"The amount of the entry in integer counts or USD Cents","gt":0},"amount_str":{"type":"string","title":"Amount Str","readOnly":true},"censored_product_user_id":{"type":"string","title":"Censored Product User Id","readOnly":true}},"type":"object","required":["entry_type","amount","amount_str","censored_product_user_id"],"title":"ContestEntry"},"ContestEntryRule":{"properties":{"max_entry_amount_per_user":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Max Entry Amount Per User","description":"Maximum total value of entries per user"},"max_daily_entries_per_user":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Max Daily Entries Per User","description":"Maximum entries per user allowed per day for this contest"},"min_completes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Completes"},"min_membership_level":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Membership Level"},"id_verified":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Id Verified"}},"type":"object","title":"ContestEntryRule","description":"Defines rules the user must meet to be allowed to enter this contest\nOnly applies if the ContestType is ENTRY!"},"ContestEntryTrigger":{"type":"string","enum":["task_complete","task_attempt","referral"],"title":"ContestEntryTrigger","description":"Defines what action/event triggers a (possible) entry into the contest (automatically).\nThis only is valid on milestone contests"},"ContestEntryType":{"type":"string","enum":["count","cash"],"title":"ContestEntryType","description":"All entries into a contest must be of the same type, and match\nthe entry_type of the Contest itself."},"ContestPrize":{"properties":{"kind":{"$ref":"#/components/schemas/ContestPrizeKind","description":"\nAllowed values: \n - __physical__ *(PHYSICAL)*: A physical prize (e.g. a iPhone, cash in the mail, dinner with Max)\n - __promotion__ *(PROMOTION)*: A promotion is a temporary or special offer that provides extra value or benefits (e.g. 20% bonus on completes for the next 7 days)\n - __cash__ *(CASH)*: Money is deposited into user's virtual wallet"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"estimated_cash_value":{"type":"integer","minimum":0.0,"title":"Estimated Cash Value","description":"Estimated cash value of prize in USDCents"},"cash_amount":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Cash Amount","description":"If the kind=ContestPrizeKind.CASH, this is the amount of the prize"},"promotion_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Promotion Id","description":"If the kind=ContestPrizeKind.PROMOTION, this is the promotion ID"},"leaderboard_rank":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Leaderboard Rank","description":"The prize is for achieving this rank in the associated leaderboard. The highest rank is 1."}},"type":"object","required":["kind","estimated_cash_value"],"title":"ContestPrize"},"ContestPrizeKind":{"type":"string","enum":["physical","promotion","cash"],"title":"ContestPrizeKind"},"ContestStatus":{"type":"string","enum":["active","completed","cancelled"],"title":"ContestStatus"},"ContestWinner":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When this user won this prize"},"prize":{"$ref":"#/components/schemas/ContestPrize"},"awarded_cash_amount":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Awarded Cash Amount","description":"The actual amount this user receives. For cash prizes, if there was a tie, this could be different from the prize amount."},"product_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Product User Id","readOnly":true}},"type":"object","required":["prize","product_user_id"],"title":"ContestWinner","description":"In a Raffle, the ContestEntryType can be COUNT or CASH. In the CASH type,\nthe unit of entry is 1 USDCent (one penny). Implicitly, each penny entered\nbuys 1 entry into the raffle, and one entry is randomly selected for\neach prize.\n\nA contest should have as many winners as there are prizes\nspecial case 1: there are fewer entries than prizes\nspecial case 2: leaderboard contest with ties"},"CountStat":{"properties":{"facet":{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"}]},"propertyNames":{"enum":["country_iso","day","month"]},"type":"object","title":"Facet","description":"The grouping criteria","examples":[{"country_iso":"us"}]},"count":{"type":"integer","title":"Count","description":"The count value for the given metric and facet"}},"type":"object","required":["facet","count"],"title":"CountStat"},"CreateCashMailCashoutMethodRequest":{"properties":{"bpuid":{"type":"string","maxLength":128,"minLength":3,"title":"Bpuid","description":"(product_user_id) The user to create this cashout method for.","examples":["app-user-9329ebd"]},"type":{"type":"string","const":"CASH_IN_MAIL","title":"Type","default":"CASH_IN_MAIL"},"delivery_address":{"$ref":"#/components/schemas/USDeliveryAddress","description":"Delivery address where payment should be sent"}},"type":"object","required":["bpuid","delivery_address"],"title":"CreateCashMailCashoutMethodRequest"},"CreateCashoutRequest":{"properties":{"bpuid":{"type":"string","maxLength":128,"minLength":3,"title":"Bpuid","description":"(product_user_id) The user requesting a cashout.","examples":["app-user-9329ebd"]},"amount":{"type":"integer","exclusiveMinimum":0.0,"title":"Amount","description":"(USD cents) The amount requested for the cashout.","examples":[531]},"cashout_method_id":{"type":"string","maxLength":32,"minLength":32,"title":"Cashout Method Id","description":"Unique ID for the cashout method the cashout is being requested with.","examples":["941d489c3ce04eb39a0ddb7f8f75db74"]}},"type":"object","required":["bpuid","amount","cashout_method_id"],"title":"CreateCashoutRequest"},"CreatePayPalCashoutMethodRequest":{"properties":{"bpuid":{"type":"string","maxLength":128,"minLength":3,"title":"Bpuid","description":"(product_user_id) The user to create this cashout method for.","examples":["app-user-9329ebd"]},"type":{"type":"string","const":"PAYPAL","title":"Type","default":"PAYPAL"},"email":{"type":"string","format":"email","title":"Email","description":"Email address of the paypal user","examples":["test@example.com"]}},"type":"object","required":["bpuid","email"],"title":"CreatePayPalCashoutMethodRequest"},"Currency":{"type":"string","enum":["USD","CAD","GBP","EUR","INR","AUD","PLN","SEK","SGD","MXN"],"title":"Currency"},"DeliveryStatus":{"type":"string","enum":["Pending","Shipped","In Transit","Out for Delivery","Delivered","Returned","Canceled","Failed Attempt","Lost"],"title":"DeliveryStatus"},"Direction":{"type":"integer","enum":[-1,1],"title":"Direction","description":"Entries on the debit side will increase debit normal accounts, while\nentries on the credit side will decrease them. Conversely, entries on\nthe credit side will increase credit normal accounts, while entries on\nthe debit side will decrease them.\n\nBy convention (?), the db will store transactions as debit-normal. For\na credit-normal account, we should flip the signs."},"DurationSummary":{"properties":{"min":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Min"},"max":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Max"},"mean":{"anyOf":[{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Mean"},"q1":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Q1"},"q2":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Q2"},"q3":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Q3"},"lower_whisker":{"type":"integer","title":"Lower Whisker","readOnly":true},"upper_whisker":{"type":"integer","title":"Upper Whisker","readOnly":true}},"type":"object","required":["min","max","q1","q2","q3","lower_whisker","upper_whisker"],"title":"DurationSummary","description":"Durations are in integer seconds.\nDescribes the statistical distribution of expected durations of tasks within this bucket.","examples":[{"max":1180,"mean":660,"min":112,"q1":457,"q2":650,"q3":1103}]},"EventEnvelope":{"properties":{"event_uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Event Uuid"},"event_type":{"$ref":"#/components/schemas/EventType"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"},"version":{"type":"integer","title":"Version","default":1},"product_user_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":3},{"type":"null"}],"title":"Product User Id","description":"A unique identifier for each user. This is hidden unlessthe event is for the requesting user.","examples":["app-user-9329ebd"]},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","examples":["4fe381fb7186416cb443a38fa66c6557"]},"payload":{"oneOf":[{"$ref":"#/components/schemas/TaskEnterPayload"},{"$ref":"#/components/schemas/TaskFinishPayload"},{"$ref":"#/components/schemas/SessionEnterPayload"},{"$ref":"#/components/schemas/SessionFinishPayload"}],"title":"Payload","discriminator":{"propertyName":"event_type","mapping":{"session.enter":"#/components/schemas/SessionEnterPayload","session.finish":"#/components/schemas/SessionFinishPayload","task.enter":"#/components/schemas/TaskEnterPayload","task.finish":"#/components/schemas/TaskFinishPayload"}}}},"type":"object","required":["event_type","product_id","payload"],"title":"EventEnvelope"},"EventMessage":{"properties":{"kind":{"type":"string","const":"event","title":"Kind","default":"event"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"},"data":{"$ref":"#/components/schemas/EventEnvelope"}},"type":"object","required":["data"],"title":"EventMessage"},"EventType":{"type":"string","enum":["task.enter","task.finish","session.enter","session.finish","wallet.credit","wallet.debit","user.created","user.active"],"title":"EventType"},"GeoIPInformation":{"properties":{"geoname_id":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Geoname Id"},"continent_code":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Continent Code"},"continent_name":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Continent Name"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso","description":"The ISO code of the country associated with the IP address.","examples":["bj"]},"country_name":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Country Name"},"subdivision_1_iso":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Subdivision 1 Iso","description":"The ISO code of the primary subdivision (e.g., state or province)."},"subdivision_1_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Subdivision 1 Name","description":"The name of the primary subdivision (e.g., state or province)."},"subdivision_2_iso":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Subdivision 2 Iso","description":"The ISO code of the secondary subdivision (if applicable)."},"subdivision_2_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Subdivision 2 Name","description":"The name of the secondary subdivision (if applicable)."},"city_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"City Name","description":"The name of the city associated with the IP address.","examples":["Jasonland"]},"metro_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Metro Code"},"time_zone":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Time Zone","description":"The time zone associated with the geographical location.","examples":["America/St_Lucia"]},"is_in_european_union":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is In European Union"},"updated":{"type":"string","format":"date-time","title":"Updated"},"ip":{"type":"string","title":"Ip"},"registered_country_iso":{"anyOf":[{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},{"type":"null"}],"title":"Registered Country Iso","description":"The ISO code of the country where the IP address is registered.","examples":["bd"]},"is_anonymous":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Anonymous","description":"Indicates whether the IP address is associated with an anonymous source (e.g., VPN, proxy).","examples":[false]},"is_anonymous_vpn":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Anonymous Vpn"},"is_hosting_provider":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Hosting Provider"},"is_public_proxy":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Public Proxy"},"is_tor_exit_node":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Tor Exit Node"},"is_residential_proxy":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Residential Proxy"},"autonomous_system_number":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Autonomous System Number"},"autonomous_system_organization":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Autonomous System Organization"},"domain":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Domain"},"isp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Isp","description":"The Internet Service Provider associated with the IP address.","examples":["Comcast"]},"mobile_country_code":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Mobile Country Code"},"mobile_network_code":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Mobile Network Code"},"network":{"anyOf":[{"type":"string","maxLength":56},{"type":"null"}],"title":"Network"},"organization":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Organization"},"static_ip_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Static Ip Score","description":"A score indicating the likelihood that the IP address is static."},"user_type":{"anyOf":[{"$ref":"#/components/schemas/AccessType"},{"type":"null"}],"description":"The type of user associated with the IP address (e.g., 'residential', 'business').","examples":["residential"]},"postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postal Code","description":"The postal code associated with the IP address.","examples":["85242"]},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latitude","description":"The latitude coordinate of the IP address location.","examples":[-67.093362]},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Longitude","description":"The longitude coordinate of the IP address location.","examples":[-66.51243]},"accuracy_radius":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Accuracy Radius","description":"The approximate radius of accuracy for the latitude and longitude, in kilometers.","examples":[73]}},"type":"object","required":["country_iso","ip"],"title":"GeoIPInformation"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"IntegrationMode":{"type":"string","enum":["platform","pass_through"],"title":"IntegrationMode"},"Leaderboard":{"properties":{"id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Id","description":"Unique ID for this leaderboard","examples":["845b0074ad533df580ebb9c80cc3bce1"]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Descriptive name for the leaderboard based on the board_code","examples":["Number of Completes"]},"board_code":{"$ref":"#/components/schemas/LeaderboardCode","description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts","examples":["complete_count"]},"bpid":{"type":"string","maxLength":32,"minLength":32,"title":"Bpid","description":"product_id","examples":["4fe381fb7186416cb443a38fa66c6557"]},"country_iso":{"type":"string","title":"Country Iso","description":"The country this leaderboard is for.","examples":["us"]},"freq":{"$ref":"#/components/schemas/LeaderboardFrequency","description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00","examples":["daily"]},"timezone_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone Name","description":"The timezone for the requested country","examples":["America/New_York"]},"sort_order":{"type":"string","enum":["ascending","descending"],"title":"Sort Order","default":"descending"},"row_count":{"type":"integer","minimum":0.0,"title":"Row Count","description":"The total number of rows in the leaderboard.","examples":[2]},"rows":{"items":{"$ref":"#/components/schemas/LeaderboardRow"},"type":"array","title":"Rows","examples":[[{"bpuid":"app-user-9329ebd","rank":1,"value":4},{"bpuid":"app-user-7923skw","rank":2,"value":3}]]},"period_start_local":{"type":"string","format":"date-time","title":"Period Start Local","description":"The start of the time period covered by this board in local time, tz-aware","examples":["2024-07-12T00:00:00-04:00"]},"period_end_local":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Period End Local","description":"The end of the time period covered by this board in local time, tz-aware","examples":["2024-07-12T23:59:59.999999-04:00"]},"start_timestamp":{"type":"integer","title":"Start Timestamp","description":"(unix timestamp) The start time of the time range this leaderboard covers.","readOnly":true,"examples":[1720756800]},"end_timestamp":{"type":"integer","title":"End Timestamp","description":"(unix timestamp) The end time of the time range this leaderboard covers.","readOnly":true,"examples":[1720843199]},"utc_offset":{"type":"string","title":"Utc Offset","description":"The UTC offset for the timezone","readOnly":true,"examples":["-0400"]},"local_start_time":{"type":"string","title":"Local Start Time","description":"The start time of the time range this leaderboard covers (local time, in the leaderboard's timezone).","readOnly":true,"examples":["2024-07-12T00:00:00-04:00"]},"local_end_time":{"type":"string","title":"Local End Time","description":"The end time of the time range this leaderboard covers (local time, in the leaderboard's timezone).","readOnly":true,"examples":["2024-07-12T23:59:59.999999-04:00"]},"start_end_str":{"type":"string","title":"Start End Str","description":"A formatted string for time period covered by this leaderboard. Can be used to display to users.","readOnly":true,"examples":["2024-02-07 to 2024-02-08"]}},"type":"object","required":["board_code","bpid","country_iso","freq","row_count","period_start_local","start_timestamp","end_timestamp","utc_offset","local_start_time","local_end_time","start_end_str"],"title":"Leaderboard","description":"A leaderboard exists independently for each product_id in each country.\nEach country is associated with a single timezone. There is a daily,\nweekly, and monthly leaderboard."},"LeaderboardCode":{"type":"string","enum":["complete_count","largest_user_payout","sum_user_payout"],"title":"LeaderboardCode","description":"The type of leaderboard. What the \"values\" represent."},"LeaderboardContestUserView":{"properties":{"name":{"type":"string","maxLength":128,"title":"Name","description":"Name of contest. Can be displayed to user."},"description":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Description","description":"Description of contest. Can be displayed to user."},"contest_type":{"type":"string","const":"leaderboard","title":"Contest Type","default":"leaderboard"},"end_condition":{"$ref":"#/components/schemas/ContestEndCondition"},"prizes":{"items":{"$ref":"#/components/schemas/ContestPrize"},"type":"array","minItems":1,"title":"Prizes"},"starts_at":{"type":"string","format":"date-time","title":"Starts At","description":"When the contest starts"},"terms_and_conditions":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Terms And Conditions"},"status":{"$ref":"#/components/schemas/ContestStatus","default":"active"},"country_isos":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Isos","description":"Contest is restricted to these countries. If null, all countries are allowed"},"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","description":"Contest applies only to a single BP"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When this contest was created"},"updated_at":{"type":"string","format":"date-time","title":"Updated At","description":"When this contest was last modified. Does not include entries being created/modified"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At","description":"When the contest ended"},"end_reason":{"anyOf":[{"$ref":"#/components/schemas/ContestEndReason"},{"type":"null"}],"description":"The reason the contest ended"},"product_user_id":{"type":"string","title":"Product User Id"},"user_winnings":{"items":{"$ref":"#/components/schemas/ContestWinner"},"type":"array","title":"User Winnings","description":"The prizes won in this contest by the requested user"},"leaderboard_key":{"type":"string","title":"Leaderboard Key","description":"The specific leaderboard instance this contest is connected to","examples":["leaderboard:7a9d8d02334449ceb105764f77e1ba97:us:weekly:2025-05-26:complete_count"]},"tie_break_strategy":{"type":"string","const":"split_prize_pool","title":"Tie Break Strategy","default":"split_prize_pool"},"country_iso":{"type":"string","title":"Country Iso","readOnly":true},"user_rank":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Rank","description":"The current rank of this user in this contest","readOnly":true}},"additionalProperties":false,"type":"object","required":["name","product_id","product_user_id","leaderboard_key","country_iso","user_rank"],"title":"LeaderboardContestUserView"},"LeaderboardFrequency":{"type":"string","enum":["daily","weekly","monthly"],"title":"LeaderboardFrequency","description":"The time period range for the leaderboard."},"LeaderboardResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"leaderboard":{"$ref":"#/components/schemas/Leaderboard"}},"type":"object","required":["status","leaderboard"],"title":"LeaderboardResponse"},"LeaderboardRow":{"properties":{"bpuid":{"type":"string","title":"Bpuid","description":"product_user_id","examples":["app-user-9329ebd"]},"rank":{"type":"integer","title":"Rank","description":"The numerical data ranks (1 through n) of the values. Ties are ranked using the lowest rank in the group.","examples":[1]},"value":{"type":"integer","title":"Value","description":"The value. The meaning of the value is dependent on the LeaderboardCode.","examples":[7]},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name","description":"Optional public name chosen by the user"}},"type":"object","required":["bpuid","rank","value"],"title":"LeaderboardRow"},"LeaderboardWinner":{"properties":{"rank":{"type":"integer","title":"Rank","description":"The user's final rank in the leaderboard","examples":[1]},"freq":{"$ref":"#/components/schemas/LeaderboardFrequency","description":"The time period range for the leaderboard.\n\nAllowed values: \n - __daily__ *(DAILY)*: UTC midnight to UTC midnight\n - __weekly__ *(WEEKLY)*: Sunday Midnight to Sunday Midnight UTC\n - __monthly__ *(MONTHLY)*: Jan 1 00:00:00","examples":["daily"]},"board_code":{"$ref":"#/components/schemas/LeaderboardCode","description":"The type of leaderboard. What the \"values\" represent.\n\nAllowed values: \n - __complete_count__ *(COMPLETE_COUNT)*: Number of Completes\n - __largest_user_payout__ *(LARGEST_PAYOUT)*: Largest Single Payout\n - __sum_user_payout__ *(SUM_PAYOUTS)*: (Sum of) Total Payouts","examples":["complete_count"]},"country_iso":{"type":"string","title":"Country Iso","description":"The country this leaderboard is for.","examples":["us"]},"issued":{"type":"string","format":"date-time","title":"Issued","description":"When the prize was issued.","examples":["2022-10-17T05:59:14.570231Z"]},"bpuid":{"type":"string","title":"Bpuid","description":"product_user_id","examples":["app-user-9329ebd"]},"description":{"type":"string","title":"Description","examples":["Bonus for daily contest"]},"amount":{"type":"integer","title":"Amount","description":"(USD cents) The reward amount","examples":[1000]},"amount_str":{"type":"string","title":"Amount Str","description":"The amount as a formatted string in USD. Can be displayed to the user.","examples":["$10.00"]},"contest_start":{"type":"string","format":"date-time","title":"Contest Start","description":"When the leaderboard started","examples":["2022-10-16T04:00:00Z"]},"contest_end":{"type":"string","format":"date-time","title":"Contest End","description":"When the leaderboard ended","examples":["2022-10-17T04:00:00Z"]}},"type":"object","required":["rank","freq","board_code","country_iso","issued","bpuid","description","amount","amount_str","contest_start","contest_end"],"title":"LeaderboardWinner"},"LeaderboardWinnerResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"winners":{"items":{"$ref":"#/components/schemas/LeaderboardWinner"},"type":"array","title":"Winners"}},"type":"object","required":["status"],"title":"LeaderboardWinnerResponse"},"LedgerAccount":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid","description":"A unique identifier for this Ledger Account","examples":["c3c3566b5b1b4961b63a5670a2dc923d"]},"display_name":{"type":"string","maxLength":64,"title":"Display Name","description":"Human-readable description of the Ledger Account","examples":["BP Wallet c3c3566b5b1b4961b63a5670a2dc923d"]},"qualified_name":{"type":"string","maxLength":255,"title":"Qualified Name"},"account_type":{"$ref":"#/components/schemas/AccountType","description":" - `bp_commission` = BP_COMMISSION\n - `bp_wallet` = BP_WALLET\n - `user_wallet` = USER_WALLET\n - `user_attempt_credit` = USER_ATTEMPT_CREDIT\n - `cash` = CASH\n - `revenue` = REVENUE\n - `expense` = EXPENSE\n - `contest_wallet` = CONTEST_WALLET\n - `credit_line` = CREDIT_LINE\n - `wa_wallet` = WA_WALLET\n - `wa_budget_pool` = WA_BUDGET_POOL\n - `wa_held` = WA_HELD\n - `wa_credit_line` = WA_CREDIT_LINE","examples":["bp_wallet"]},"normal_balance":{"$ref":"#/components/schemas/Direction","description":" - `-1` = CREDIT\n - `1` = DEBIT"},"reference_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference Type"},"reference_uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Reference Uuid","description":"The associated Product ID or other parent account thatthis Ledger Account is intended to track transactions for.If Wallet mode is enabled, this can also handle trackingindividual users.","examples":["61dd0b086fd048518762757612b4a6d3"]},"currency":{"type":"string","maxLength":32,"title":"Currency","description":"GRL's Ledger system allows tracking of transactions inany currency possible. This is useful for trackingpoints, stars, coins, or any other currency that may beused in a Supplier's platform.","default":"USD"}},"type":"object","required":["display_name","qualified_name","account_type","normal_balance"],"title":"LedgerAccount"},"LedgerEntry":{"properties":{"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"direction":{"$ref":"#/components/schemas/Direction"},"account_uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Account Uuid"},"amount":{"type":"integer","exclusiveMaximum":9.223372036854776e+18,"exclusiveMinimum":0.0,"title":"Amount","description":"The USDCent amount. A LedgerEntry cannot be made for0 USDCent and it cannot be negative.","examples":[531]},"transaction_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Transaction Id"}},"type":"object","required":["direction","account_uuid","amount"],"title":"LedgerEntry"},"LedgerTransaction":{"properties":{"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"created":{"type":"string","format":"date-time","title":"Created","description":"When the Transaction (TX) was created into the database.This does not represent the exact time for any actionwhich may be responsible for this Transaction (TX), and TX timestamps will likely be a few milliseconds delayed"},"ext_description":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Ext Description"},"tag":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Tag"},"metadata":{"additionalProperties":{"type":"string"},"type":"object","title":"Metadata"},"entries":{"items":{"$ref":"#/components/schemas/LedgerEntry"},"type":"array","title":"Entries","description":"A Transaction (TX) is composed of multiple Entry events."}},"additionalProperties":false,"type":"object","title":"LedgerTransaction"},"ManageCashoutBody":{"properties":{"payout_id":{"type":"string","maxLength":32,"minLength":32,"title":"Payout Id"},"new_status":{"$ref":"#/components/schemas/PayoutStatus"}},"type":"object","required":["payout_id","new_status"],"title":"ManageCashoutBody"},"MarketProfileKnowledge":{"properties":{"source":{"$ref":"#/components/schemas/Source","maxLength":16,"description":"Marketplace this question is from"},"question_id":{"type":"string","title":"Question Id","examples":["gender","1843","gender_plus"]},"answer":{"items":{"type":"string"},"type":"array","title":"Answer","examples":[["male"],["7657644"],["1"]]},"created":{"type":"string","format":"date-time","title":"Created","description":"When the User submitted this Profiling data"}},"type":"object","required":["source","question_id","created"],"title":"MarketProfileKnowledge","description":"This is used solely in API responses, so it is simplified."},"MarketplaceBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"x":{"type":"integer","minimum":0.0,"title":"X","description":"For UI. Provides a dimensionality position for the bucket on the x-axis.","default":0,"examples":[0,1,2]},"y":{"type":"integer","minimum":0.0,"title":"Y","description":"For UI. Provides a dimensionality position for the bucket on the y-axis.","default":0,"examples":[0,1,2]},"name":{"type":"string","title":"Name","description":"Currently unused. Will always return empty string","default":""},"description":{"type":"string","title":"Description","description":"Currently unused. Will always return empty string","default":""},"category":{"items":{"$ref":"#/components/schemas/CategoryAssociation"},"type":"array","title":"Category"},"contents":{"items":{"$ref":"#/components/schemas/BucketTask"},"type":"array","title":"Contents"},"duration":{"$ref":"#/components/schemas/DurationSummary"},"payout":{"$ref":"#/components/schemas/PayoutSummary"},"source":{"type":"string","enum":["innovate","dynata","schlesinger","purespectrum","morning","pollfish","precision","repdata","prodege"],"title":"Source","description":"this is the source of the contents","examples":["pollfish"]}},"additionalProperties":false,"type":"object","required":["id","uri","contents","duration","payout","source"],"title":"MarketplaceBucket"},"MarketplaceInventorySummary":{"properties":{"live_tasks":{"items":{"$ref":"#/components/schemas/CountStat"},"type":"array","title":"Live Tasks","description":"The count of tasks that are currently live"},"live_gen_pop_tasks":{"items":{"$ref":"#/components/schemas/CountStat"},"type":"array","title":"Live Gen Pop Tasks","description":"The count of gen-pop tasks that are currently live"},"tasks_created":{"items":{"$ref":"#/components/schemas/CountStat"},"type":"array","title":"Tasks Created","description":"The count of tasks created"},"required_finishes":{"items":{"$ref":"#/components/schemas/CountStat"},"type":"array","title":"Required Finishes","description":"Number of finishes needed across all live tasks"},"payout":{"items":{"$ref":"#/components/schemas/StatisticalSummaryStat"},"type":"array","title":"Payout","description":"The distribution of payouts for all live tasks"},"expected_duration":{"items":{"$ref":"#/components/schemas/StatisticalSummaryStat"},"type":"array","title":"Expected Duration","description":"The distribution of expected durations for all live tasks"},"required_finishes_per_task":{"items":{"$ref":"#/components/schemas/StatisticalSummaryStat"},"type":"array","title":"Required Finishes Per Task","description":"The distribution of required finishes on all live tasks"}},"type":"object","title":"MarketplaceInventorySummary"},"MarketplaceOfferwall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/MarketplaceBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"MarketplaceOfferwall","description":"Returns buckets grouped by marketplace, one per marketplace, with the\ntasks ordered by quality.\n\nOfferwall code: `5fa23085`"},"MarketplaceOfferwallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/MarketplaceOfferwall"}},"type":"object","required":["info","offerwall"],"title":"MarketplaceOfferwallResponse"},"MarketplaceSummary":{"properties":{"inventory":{"$ref":"#/components/schemas/MarketplaceInventorySummary","description":"Inventory of the marketplace"},"user_activity":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Activity","description":"User activity of the marketplace"}},"type":"object","required":["inventory"],"title":"MarketplaceSummary"},"MaxGaugeBySource":{"properties":{"value":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Value"},"by_source":{"additionalProperties":{"type":"integer","minimum":0.0},"propertyNames":{"$ref":"#/components/schemas/Source"},"type":"object","title":"By Source"}},"type":"object","title":"MaxGaugeBySource"},"MilestoneContestEndCondition":{"properties":{"max_winners":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Max Winners","description":"The contest will end once this many users have won (i.e. reachedthe milestone)."},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At","description":"The Contest is over at the ends_at time."}},"type":"object","title":"MilestoneContestEndCondition","description":"Defines the conditions to evaluate to determine when the contest is over.\nMultiple conditions can be set. The contest is over once ANY conditions are met."},"MilestoneUserView":{"properties":{"target_amount":{"type":"integer","exclusiveMinimum":0.0,"title":"Target Amount","description":"Each user 'wins' (receives prizes) once this target amount is reached."},"entry_trigger":{"anyOf":[{"$ref":"#/components/schemas/ContestEntryTrigger"},{"type":"null"}],"description":"What user action triggers an entry automatically."},"valid_for":{"anyOf":[{"type":"string","format":"duration"},{"type":"null"}],"title":"Valid For","description":"The time after valid_for_event for which the contest is open"},"valid_for_event":{"anyOf":[{"type":"string","const":"signup"},{"type":"null"}],"title":"Valid For Event"},"name":{"type":"string","maxLength":128,"title":"Name","description":"Name of contest. Can be displayed to user."},"description":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Description","description":"Description of contest. Can be displayed to user."},"contest_type":{"type":"string","const":"milestone","title":"Contest Type","default":"milestone"},"end_condition":{"$ref":"#/components/schemas/MilestoneContestEndCondition"},"prizes":{"items":{"$ref":"#/components/schemas/ContestPrize"},"type":"array","minItems":1,"title":"Prizes"},"starts_at":{"type":"string","format":"date-time","title":"Starts At","description":"When the contest starts"},"terms_and_conditions":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Terms And Conditions"},"status":{"$ref":"#/components/schemas/ContestStatus","default":"active"},"country_isos":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Isos","description":"Contest is restricted to these countries. If null, all countries are allowed"},"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","description":"Contest applies only to a single BP"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When this contest was created"},"updated_at":{"type":"string","format":"date-time","title":"Updated At","description":"When this contest was last modified. Does not include entries being created/modified"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At","description":"When the contest ended"},"end_reason":{"anyOf":[{"$ref":"#/components/schemas/ContestEndReason"},{"type":"null"}],"description":"The reason the contest ended"},"product_user_id":{"type":"string","title":"Product User Id"},"user_winnings":{"items":{"$ref":"#/components/schemas/ContestWinner"},"type":"array","title":"User Winnings","description":"The prizes won in this contest by the requested user"},"entry_type":{"type":"string","const":"count","title":"Entry Type","default":"count"},"win_count":{"type":"integer","title":"Win Count","description":"The number of times the milestone has been reached.","default":0},"user_amount":{"type":"integer","title":"User Amount","description":"The total amount for this user for this contest"}},"additionalProperties":false,"type":"object","required":["target_amount","name","end_condition","product_id","product_user_id","user_amount"],"title":"MilestoneUserView"},"OfferWallCategoryRequest":{"properties":{"adwords_category":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Adwords Category","examples":[["45","65"]]},"category":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Category","examples":[["98c137e4e90a4d92ac6c00e523eb1b50"]]},"exclude_adwords_category":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Exclude Adwords Category","examples":[["1558"]]},"exclude_category":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Exclude Category","examples":[["21536f160f784189be6194ca894f3a65","7aa8bf4e71a84dc3b2035f93f9f9c77e"]]}},"type":"object","title":"OfferWallCategoryRequest"},"OfferWallInfo":{"properties":{"success":{"type":"boolean","title":"Success"}},"type":"object","required":["success"],"title":"OfferWallInfo"},"OfferWallRequestYieldmanParams":{"properties":{"conversion_factor_adj":{"type":"number","title":"Conversion Factor Adj","description":"Centered around 0. Higher results in higher weight given to conversion (in the scoring function)","default":0.0},"dropoffrate_factor_adj":{"type":"number","title":"Dropoffrate Factor Adj","description":"Centered around 0. Higher results in higher penalty given to dropoffs (in the scoring function)","default":0.0},"longfail_factor_adj":{"type":"number","title":"Longfail Factor Adj","description":"Centered around 0. Higher results in higher penalty given to long fail (in the scoring function)","default":0.0},"recon_factor_adj":{"type":"number","title":"Recon Factor Adj","description":"Centered around 0. Higher results in higher penalty given to recons (in the scoring function)","default":0.0},"recon_likelihood_max":{"type":"number","title":"Recon Likelihood Max","description":"Tolerance for recon likelihood (0 to 1)","default":0.8}},"type":"object","title":"OfferWallRequestYieldmanParams"},"OfferwallConfig":{"properties":{},"type":"object","title":"OfferwallConfig"},"OfferwallReason":{"type":"string","enum":["USER_BLOCKED","HIGH_RECON_RATE","UNCOMMON_DEMOGRAPHICS","UNDER_MINIMUM_AGE","EXHAUSTED_HIGH_VALUE_SUPPLY","ALL_ELIGIBLE_ATTEMPTED","LOW_CURRENT_SUPPLY"],"title":"OfferwallReason"},"OneShotOfferwall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/OneShotOfferwallBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"OneShotOfferwall","description":"Each bucket has only 1 single task, and only basic info is returned\n about each bucket.\n\nOfferwall code: `6f27b1ae`"},"OneShotOfferwallBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"duration":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Duration","description":"The bucket's expected duration, in seconds"},"min_payout":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Min Payout","description":"The bucket's min payout, in usd cents"}},"additionalProperties":false,"type":"object","required":["id","uri","duration","min_payout"],"title":"OneShotOfferwallBucket"},"OneShotOfferwallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/OneShotOfferwall"}},"type":"object","required":["info","offerwall"],"title":"OneShotOfferwallResponse"},"POPFinancial":{"properties":{"product_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Product Id","examples":["acdb85c0874547659084ac88049e1bb1"]},"time":{"type":"string","format":"date-time","title":"Time","description":"The starting time block for the respective 'Period' thatthis grouping is on. The `time` could be the start of a 1 minute or 1 hour block for example."},"payout":{"type":"integer","minimum":0.0,"title":"Payout","description":"The total amount (in USD cents) that the Brokerage Producthas earned within a respective time period.","default":0,"examples":[59050]},"adjustment":{"type":"integer","title":"Adjustment","description":"The total amount (in USD cents) that the Brokerage Producthas had adjusted within a respective time period. Most ofthe time, this will be negative due to Complete to Incomplete reconciliations. However, it can also be positive due to Incomplete to Complete adjustments.","examples":[464]},"adjustment_types":{"items":{"$ref":"#/components/schemas/AdjustmentType"},"type":"array","title":"Adjustment Types"},"expense":{"type":"integer","title":"Expense","description":"For Product accounts that are setup with Respondent payouts,competitions, user bonuses, or other associated 'costs', thoseexpenses are accounted for here. This will be negative forthose types of costs."},"net":{"type":"integer","title":"Net","description":"This is the sum of the Payout total, Adjustment and any Expenses total. It can be positive or negative for any specific time period.","examples":[59514]},"payment":{"type":"integer","title":"Payment","description":"Any ACH or Wire amount that was issued between GRL and the Supplier.","examples":[3408288]}},"type":"object","required":["time","adjustment","adjustment_types","expense","net","payment"],"title":"POPFinancial","description":"We can't use our USDCent class in here because aside from it not\n supporting negative values for our adjustments, FastAPI also\n complains because it doesn't know how to generate documentation\n for it. - Max 2024-06-25"},"PatternValidation":{"properties":{"message":{"type":"string","title":"Message","description":"Message to display if validation fails"},"pattern":{"type":"string","title":"Pattern","description":"Regex string to validate. min_length and max_length are checked separately, even if they are part of the regex."}},"type":"object","required":["message","pattern"],"title":"PatternValidation"},"PayoutConfig":{"properties":{"payout_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payout Format","description":"\nThe format describing the str representation of a payout. Typically, this would be displayed to a user.\nThe payout_format is similar to python format string with a subset of functionality supported.\nOnly float with a precision are supported along with an optional comma for a thousands separator.\nIn addition, a mathematical operator can be applied, such as dividing by 100. \nExamples are shown assuming payout = 100 (one dollar).\n- \"{payout*10:,.0f} Points\" -> \"1,000 Points\"\n- \"${payout/100:.2f}\" -> \"$1.00\"\n","examples":["{payout*10:,.0f} Points","${payout/100:.2f}","{payout:.0f}"]},"payout_transformation":{"anyOf":[{"$ref":"#/components/schemas/PayoutTransformation"},{"type":"null"}],"description":"How the BP's payout is converted to the User's payout"}},"type":"object","title":"PayoutConfig","description":"Store configuration related to payouts, payout transformation, and user\npayout formatting."},"PayoutStatus":{"type":"string","enum":["PENDING","APPROVED","REJECTED","CANCELLED","FAILED","COMPLETE"],"title":"PayoutStatus","description":"The max size of the db field that holds this value is 20, so please\ndon't add new values longer than that!"},"PayoutSummary":{"properties":{"min":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Min"},"max":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Max"},"mean":{"anyOf":[{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Mean"},"q1":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Q1"},"q2":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Q2"},"q3":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Q3"},"lower_whisker":{"type":"integer","title":"Lower Whisker","readOnly":true},"upper_whisker":{"type":"integer","title":"Upper Whisker","readOnly":true}},"type":"object","required":["min","max","q1","q2","q3","lower_whisker","upper_whisker"],"title":"PayoutSummary","description":"Payouts are in Integer USD Cents","examples":[{"max":132,"min":14,"q1":45,"q2":68,"q3":124}]},"PayoutTransformation":{"properties":{"f":{"type":"string","enum":["payout_transformation_percent","payout_transformation_amt"],"title":"F","description":"The name of the transformation function to use."},"kwargs":{"anyOf":[{"$ref":"#/components/schemas/PayoutTransformationPercentArgs"},{"type":"null"}],"description":"The kwargs to pass to the transformation function.","examples":[{"max_payout":"5.00","pct":0.5}]}},"type":"object","required":["f"],"title":"PayoutTransformation","description":"This model describe how the bp_payout is converted to the user_payout.\nIf None, the user_payout is None.\n\nIf the user_wallet_enabled is `False`, the user_payout is used to\n 1) know how to transform the expected payouts for offerwall buckets\n (if min_payout is requested, this is based on the user_payout)\n 2) show the user (using the payout_format) how much they made (in\n the Task Status Response).\n\nIf the user_wallet_enabled is `True`, then in addition to the above, the\n user_payout is the amount actually paid to the user's wallet."},"PayoutTransformationPercentArgs":{"properties":{"pct":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Pct","description":"The percentage of the bp_payout to pay the user","examples":[0.5]},"min_payout":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Min Payout","description":"The minimum amount paid for a complete. Note: This does not check that the actual payout was at least this amount.","examples":["0.50"]},"max_payout":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Max Payout","description":"The maximum amount paid for a complete","examples":["5.00"]}},"type":"object","required":["pct"],"title":"PayoutTransformationPercentArgs"},"PayoutType":{"type":"string","enum":["PAYPAL","TANGO","DWOLLA","ACH","WIRE","CASH_IN_MAIL","PRIZE","AMT","AMT_BONUS","AMT_ASSIGNMENT","AMT_ASSIGNMENT"],"title":"PayoutType","description":"The method in which the requested payout is delivered."},"PaypalCashoutMethodData":{"properties":{"type":{"type":"string","const":"PAYPAL","title":"Type","default":"PAYPAL"},"email":{"type":"string","format":"email","title":"Email","description":"Email address of the paypal user","examples":["test@example.com"]}},"type":"object","required":["email"],"title":"PaypalCashoutMethodData"},"PingMessage":{"properties":{"kind":{"type":"string","const":"ping","title":"Kind","default":"ping"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"}},"type":"object","title":"PingMessage"},"PongMessage":{"properties":{"kind":{"type":"string","const":"pong","title":"Kind","default":"pong"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"}},"type":"object","title":"PongMessage"},"Product":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier of the Brokerage Product","examples":["1108d053e4fa47c5b0dbdcd03a7981e7"]},"name":{"type":"string","maxLength":255,"minLength":3,"title":"Name","description":"Name of the Brokerage Product. Must be unique within a Team","examples":["Website ABC"]},"enabled":{"type":"boolean","title":"Enabled","description":"This is only used to hard block a Product in order to immediately & safely protect against fraud entrances.","default":true},"payments_enabled":{"type":"boolean","title":"Payments Enabled","description":"This is only to determine if ACH or Wire payments should be made to the Product.","default":true},"created":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created","description":"When the Product was created, this does necessarily mean it started to retrieve traffic at that time."},"team_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Team Id","description":"The organization (group of generalresearch.com admin accounts) that is allowed to modify and manage thisProduct","examples":["b96c1209cf4a4baaa27d38082421a039"]},"business_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Business Id","description":"The legal business entity or individual that is responsible for this account, and that receive SupplierPayments for this Product's activity.","examples":["26bac7a8244b4c0da0c31384e3789fe3"]},"tags":{"items":{"$ref":"#/components/schemas/SupplierTag"},"type":"array","uniqueItems":true,"title":"Tags","description":"Tags which are used to annotate supplier traffic"},"commission_pct":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*(?:\\d{0,1}|(?=[\\d.]{1,7}0*$)\\d{0,1}\\.\\d{0,5}0*$)","title":"Commission Pct","default":"0.05"},"redirect_url":{"type":"string","title":"Redirect Url","description":"Where to redirect the user after finishing a session. When a user get redirected back to the supplier, a query param will be added with the name 'tsid', and the value of the TSID for the session. For example: callback_uri: 'https://www.example.com/test/?a=1&b=2' might result in the user getting redirected to: 'https://www.example.com/grl-callback/?a=1&b=2&tsid=c6ab6ba1e75b44e2bf5aab00fc68e3b7'.","examples":["https://www.example.com/grl-callback/?a=1&b=2"]},"harmonizer_domain":{"type":"string","title":"Harmonizer Domain","description":"This is the domain that is used for the GRS (General Research Survey) platform. This is a simple javascript application which may profile the respondent for anyprofiling questions, along with collecting any browserbased security information. The value is a scheme+domain only (no path).","default":"https://profile.generalresearch.com/"},"sources_config":{"anyOf":[{"$ref":"#/components/schemas/SourcesConfig"},{"$ref":"#/components/schemas/SupplyConfig"}],"title":"Sources Config"},"session_config":{"$ref":"#/components/schemas/SessionConfig"},"payout_config":{"$ref":"#/components/schemas/PayoutConfig"},"user_wallet_config":{"$ref":"#/components/schemas/UserWalletConfig"},"user_create_config":{"$ref":"#/components/schemas/UserCreateConfig"},"offerwall_config":{"$ref":"#/components/schemas/OfferwallConfig"},"profiling_config":{"$ref":"#/components/schemas/ProfilingConfig"},"user_health_config":{"$ref":"#/components/schemas/UserHealthConfig"},"yield_man_config":{"$ref":"#/components/schemas/YieldManConfig"},"balance":{"anyOf":[{"$ref":"#/components/schemas/ProductBalances"},{"type":"null"}],"description":"Product Balance"},"payouts_total_str":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payouts Total Str"},"payouts_total":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Payouts Total"},"payouts":{"anyOf":[{"items":{"$ref":"#/components/schemas/BrokerageProductPayoutEvent"},"type":"array"},{"type":"null"}],"title":"Payouts","description":"Product Payouts. These are the ACH or Wire payments that were sent to theBusiness on behalf of this specific Product"},"pop_financial":{"anyOf":[{"items":{"$ref":"#/components/schemas/POPFinancial"},"type":"array"},{"type":"null"}],"title":"Pop Financial"},"bp_account":{"anyOf":[{"$ref":"#/components/schemas/LedgerAccount"},{"type":"null"}]},"user_wallet":{"$ref":"#/components/schemas/UserWalletConfig","readOnly":true}},"type":"object","required":["name","redirect_url","user_wallet"],"title":"Product"},"ProductBalances":{"properties":{"product_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Product Id","examples":["7e94ebe63bbc4509876c4914cc61a984"]},"last_event":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Event"},"bp_payment_credit":{"type":"integer","minimum":0.0,"title":"Bp Payment Credit","description":"The total amount that has been earned by the Task completes, for this Brokerage Product account.","default":0,"examples":[18837]},"adjustment_credit":{"type":"integer","minimum":0.0,"title":"Adjustment Credit","description":"Positive reconciliations issued back to the Brokerage Product account.","default":0,"examples":[2]},"adjustment_debit":{"type":"integer","minimum":0.0,"title":"Adjustment Debit","description":"Negative reconciliations for any Task completes","default":0,"examples":[753]},"supplier_credit":{"type":"integer","minimum":0.0,"title":"Supplier Credit","description":"ACH or Wire amounts issued to GRL from a Supplier to recoup for a negative Brokerage Product balance","default":0,"examples":[0]},"supplier_debit":{"type":"integer","minimum":0.0,"title":"Supplier Debit","description":"ACH or Wire amounts sent to a Supplier","default":0,"examples":[10000]},"user_bonus_credit":{"type":"integer","minimum":0.0,"title":"User Bonus Credit","description":"If a respondent ever pays back an product account.","default":0,"examples":[0]},"user_bonus_debit":{"type":"integer","minimum":0.0,"title":"User Bonus Debit","description":"Pay a user into their wallet balance. There is no fee here. There is only a fee when the user requests a payout.The bonus could be as a bribe, winnings for a contest, leaderboard, etc.","default":0,"examples":[2745]},"issued_payment":{"type":"integer","minimum":0.0,"title":"Issued Payment","description":"This is the amount that we decide to credit as havingtaken from this Product. If there is any amount not issuedit is summed up over the Business to offset any negativebalances elsewhere.","default":0},"payout":{"type":"integer","title":"Task Payouts","description":"The sum amount of all Task payouts","readOnly":true,"examples":[18837]},"payout_usd_str":{"type":"string","title":"Task Payouts USD Str","readOnly":true,"examples":["$18,837.00"]},"adjustment":{"type":"integer","title":"Task Adjustments","description":"The sum amount of all Task Adjustments","readOnly":true,"examples":[-751]},"expense":{"type":"integer","title":"Product Expenses","description":"The sum amount of any associated Product Expenses (eg: user bonuses)","readOnly":true,"examples":[-2745]},"net":{"type":"integer","title":"Net Earnings","description":"The Product's Net Earnings which is equal to the totalamount of Task Payouts, with Task Adjustments and anyProduct Expenses deducted. This can be positive ornegative.","readOnly":true,"examples":[15341]},"payment":{"type":"integer","minimum":0.0,"title":"Supplier Payments","description":"The sum amount of all Supplier Payments (eg ACH or Wire transfers)","readOnly":true,"examples":[10000]},"payment_usd_str":{"type":"string","title":"Supplier Payments","readOnly":true,"examples":["$10,000"]},"balance":{"type":"integer","title":"Product Balance","description":"The Product's Balance which is equal to the Product's Netamount with already issued Supplier Payments deducted. This can be positive or negative.","readOnly":true,"examples":[5341]},"retainer":{"type":"integer","minimum":0.0,"title":"Smart Retainer","description":"The Smart Retainer is an about of money that is held byGRL to account for any Task Adjustments that may occurin the future. The amount will always be positive, andif the Product's balance is negative, the retainer will be $0.00 as the Product is not eligible for any SupplierPayments either way.","readOnly":true,"examples":[1335]},"retainer_usd_str":{"type":"string","title":"Smart Retainer USD Str","readOnly":true,"examples":["$1,335.00"]},"available_balance":{"type":"integer","minimum":0.0,"title":"Available Balance","description":"The Available Balance is the amount that is currently, andimmediately available for withdraw from the Supplier'sbalance. Supplier Payments are made every Friday for Businesses with an ACH connected Bank Account to GRL, while a Business that requires an International Wire are issued on the last Friday of every Month.","readOnly":true,"examples":[4006]},"available_balance_usd_str":{"type":"string","title":"Available Balance USD Str","readOnly":true,"examples":["$4,006.00"]},"recoup":{"type":"integer","minimum":0.0,"title":"Recoup","description":"The amount that must be held back to recover a negative Product balance. This is $0.00 when the Product balance is positive or zero; otherwise it is the absolute value of the negative balance. Business-level payout calculations use this amount to offset deficits from one Product against available funds from other Products before issuing Supplier Payments.","readOnly":true,"examples":[282]},"recoup_usd_str":{"type":"string","title":"Recoup Str","readOnly":true,"examples":["$2.04"]},"adjustment_percent":{"type":"number","title":"Adjustment Percentage","description":"The percentage of USDCent value that has been adjustedover all time for this Product.","readOnly":true,"examples":[0.064938]}},"type":"object","required":["payout","payout_usd_str","adjustment","expense","net","payment","payment_usd_str","balance","retainer","retainer_usd_str","available_balance","available_balance_usd_str","recoup","recoup_usd_str","adjustment_percent"],"title":"ProductBalances"},"ProfilingConfig":{"properties":{"enabled":{"type":"boolean","title":"Enabled","description":"If False, the harmonizer/profiling system is not used at all. This should never be False unless special circumstances","default":true},"grs_enabled":{"type":"boolean","title":"Grs Enabled","description":"If grs_enabled is False, and is_grs is passed in the \n profiling-questions call, then don't actually return any questions. \n This allows a client to hit the endpoint with no limit and still get \n questions. In effect, this means that we'll redirect the user through \n the GRS system but won't present them any questions.","default":true},"n_questions":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"N Questions","description":"Use to hard code the number of questions to ask. None means use default algorithm."},"max_questions":{"type":"integer","exclusiveMinimum":0.0,"title":"Max Questions","description":"The max number of questions we would ask in a session","default":10},"avg_question_count":{"type":"number","exclusiveMinimum":0.0,"title":"Avg Question Count","description":"The average number of questions to ask in a session","default":5},"task_injection_freq_mult":{"type":"number","exclusiveMinimum":0.0,"title":"Task Injection Freq Mult","description":"Scale how frequently we inject profiling questions, \n relative to the default. 1 is default, 2 is twice as often. 10 means \n always. 0.5 half as often","default":1},"non_us_mult":{"type":"number","exclusiveMinimum":0.0,"title":"Non Us Mult","description":"Non-us multiplier, used to increase freq and length of \n profilers in all non-us countries. This value is multiplied by \n task_injection_freq_mult and avg_question_count.","default":2},"hidden_questions_expiration_hours":{"type":"integer","exclusiveMinimum":0.0,"title":"Hidden Questions Expiration Hours","description":"How frequently we should refresh hidden questions","default":168}},"type":"object","title":"ProfilingConfig"},"ProfilingInfoResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"profiling-info":{"items":{"$ref":"#/components/schemas/UpkProperty"},"type":"array","title":"Profiling-Info"}},"type":"object","required":["status","profiling-info"],"title":"ProfilingInfoResponse"},"PropertyType":{"type":"string","enum":["i","n","x"],"title":"PropertyType"},"RaffleUserView":{"properties":{"name":{"type":"string","maxLength":128,"title":"Name","description":"Name of contest. Can be displayed to user."},"description":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Description","description":"Description of contest. Can be displayed to user."},"contest_type":{"type":"string","const":"raffle","title":"Contest Type","default":"raffle"},"end_condition":{"$ref":"#/components/schemas/ContestEndCondition"},"prizes":{"items":{"$ref":"#/components/schemas/ContestPrize"},"type":"array","minItems":1,"title":"Prizes"},"starts_at":{"type":"string","format":"date-time","title":"Starts At","description":"When the contest starts"},"terms_and_conditions":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Terms And Conditions"},"status":{"$ref":"#/components/schemas/ContestStatus","default":"active"},"country_isos":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country Isos","description":"Contest is restricted to these countries. If null, all countries are allowed"},"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","description":"Contest applies only to a single BP"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When this contest was created"},"updated_at":{"type":"string","format":"date-time","title":"Updated At","description":"When this contest was last modified. Does not include entries being created/modified"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At","description":"When the contest ended"},"end_reason":{"anyOf":[{"$ref":"#/components/schemas/ContestEndReason"},{"type":"null"}],"description":"The reason the contest ended"},"product_user_id":{"type":"string","title":"Product User Id"},"user_winnings":{"items":{"$ref":"#/components/schemas/ContestWinner"},"type":"array","title":"User Winnings","description":"The prizes won in this contest by the requested user"},"entry_type":{"type":"string","const":"cash","title":"Entry Type","default":"cash"},"entry_rule":{"$ref":"#/components/schemas/ContestEntryRule"},"current_amount":{"anyOf":[{"type":"integer"},{"type":"integer","minimum":0.0}],"title":"Current Amount","description":"Sum of all entry amounts","default":0},"current_participants":{"type":"integer","title":"Current Participants","description":"Count of unique participants","default":0},"user_amount":{"anyOf":[{"type":"integer"},{"type":"integer","minimum":0.0}],"title":"User Amount","description":"The total amount this user has entered"},"user_amount_today":{"anyOf":[{"type":"integer"},{"type":"integer","minimum":0.0}],"title":"User Amount Today","description":"The total amount this user has entered in the past 24 hours"},"current_win_probability":{"type":"number","title":"Current Win Probability","description":"Probability of this user winning 1 or more prizes, if the contestended right now","readOnly":true},"projected_win_probability":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Projected Win Probability","description":"Probability of this user winning 1 or more prizes, once the contestis projected to end. This value is only calculated if the contest has a target_entry_amountend condition.","readOnly":true}},"additionalProperties":false,"type":"object","required":["name","end_condition","product_id","product_user_id","user_amount","user_amount_today","current_win_probability","projected_win_probability"],"title":"RaffleUserView"},"RedemptionCurrency":{"type":"string","enum":["USD","CAD","GBP","EUR","INR","AUD","PLN","SEK","SGD","MXN"],"title":"RedemptionCurrency","description":"Supported Currencies for Foreign Redemptions"},"ReportTask":{"properties":{"bpuid":{"type":"string","maxLength":128,"minLength":3,"title":"product_user_id","description":"The unique identifier for the user, which is set by the Supplier.","examples":["app-user-9329ebd"]},"reasons":{"items":{"$ref":"#/components/schemas/ReportValue"},"type":"array","title":"Reasons","description":"The reason a user reported a task.\n\nAllowed values: \n - __0__ *(REASON_UNKNOWN)*: Used to indicate the user exited the task without giving feedback\n - __1__ *(TECHNICAL_ERROR)*: Task is in the wrong language/country, unanswerable question, won't proceed to next question, loading forever, error message\n - __2__ *(NO_REDIRECT)*: Task ended (completed or failed, and showed the user some dialog indicating the task was over), but failed to redirect\n - __3__ *(PRIVACY_INVASION)*: Asked for full name, home address, identity on another site, cc\n - __4__ *(UNCOMFORTABLE_TOPICS)*: Asked about children, employer, medical issues, drug use, STDs, etc.\n - __5__ *(ASKED_FOR_NOT_ALLOWED_ACTION)*: Asked to install software, signup/login to external site, access webcam, promise to pay using external site, etc.\n - __6__ *(BAD_ON_MOBILE)*: Task doesn't work well on a mobile device\n - __7__ *(DIDNT_LIKE)*: Too long, too boring, confusing, complicated, too many open-ended/free-response questions","examples":[[3,4]]},"notes":{"type":"string","title":"Notes","default":"","examples":["The survey wanted to watch me eat Haejang-guk"]}},"additionalProperties":false,"type":"object","required":["bpuid"],"title":"ReportTask"},"ReportValue":{"type":"integer","enum":[0,1,2,3,4,5,6,7],"title":"ReportValue","description":"The reason a user reported a task."},"Scope":{"type":"string","enum":["global","team","product"],"title":"Scope"},"SessionAdjustedStatus":{"type":"string","enum":["ac","af","pa"],"title":"SessionAdjustedStatus","description":"An adjusted_status is set if a session is adjusted by the marketplace\nafter the original return. A session can be adjusted multiple times.\nThis is the most recent status. If a session was originally a complete,\nwas adjusted to incomplete, then back to complete, the adjusted_status\nwill be None, but the adjusted_timestamp will be set to the most recent\nchange."},"SessionConfig":{"properties":{"max_session_len":{"type":"integer","maximum":5400.0,"minimum":60.0,"title":"Max Session Len","description":"The amount of time (in seconds) that a respondent may spend attempting to get into a survey within a session.If NULL, there is no limit.","default":600},"max_session_hard_retry":{"type":"integer","minimum":0.0,"title":"Max Session Hard Retry","description":"The number of surveys that a respondent may attempt within a session before the session is terminated.","default":5},"min_payout":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Min Payout","description":"The minimum amount the user should be paid for a complete. If \n no payout transformation is defined, the value is based on the BP's payout. \n If a payout transformation is defined, the min_payout is applied on the \n user's payout. Note, this is separate and distinct from the payout \n transformation's min payout. The payout transformation's min_payout does not \n care what the task's actual payout was. This min_payout will prevent\n the user from being show any tasks that would pay below this amount.","default":"0.14","examples":["0.50"]}},"type":"object","title":"SessionConfig","description":"Stores configuration related to the Session, a session being a users\nexperience attempting to do work."},"SessionEnterPayload":{"properties":{"event_type":{"type":"string","const":"session.enter","title":"Event Type","default":"session.enter"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"}},"type":"object","required":["country_iso"],"title":"SessionEnterPayload"},"SessionFinishPayload":{"properties":{"event_type":{"type":"string","const":"session.finish","title":"Event Type","default":"session.finish"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"},"duration_sec":{"type":"number","exclusiveMinimum":0.0,"title":"Duration Sec"},"status":{"$ref":"#/components/schemas/Status"},"status_code_1":{"anyOf":[{"$ref":"#/components/schemas/StatusCode1"},{"type":"null"}]},"status_code_2":{"anyOf":[{"$ref":"#/components/schemas/SessionStatusCode2"},{"type":"null"}]},"user_payout":{"anyOf":[{"type":"integer","maximum":4000.0,"minimum":0.0},{"type":"null"}],"title":"User Payout"}},"type":"object","required":["country_iso","duration_sec","status"],"title":"SessionFinishPayload"},"SessionStatusCode2":{"type":"integer","enum":[1,2,3,4,5,6,7,8,9,10,11,12,13],"title":"SessionStatusCode2","description":"__Status Detail__\nThis should be set if the Session.status_code_1 is SESSION_XXX_FAIL"},"ShippingCarrier":{"type":"string","enum":["USPS","FedEx","UPS","DHL"],"title":"ShippingCarrier"},"ShippingMethod":{"type":"string","enum":["Standard","Express","Two-Day","Overnight","Same Day"],"title":"ShippingMethod"},"SoftPairBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way. If the eligibility is conditional or ineligible, the uri will be null.","examples":[null]},"category":{"items":{"$ref":"#/components/schemas/CategoryAssociation"},"type":"array","title":"Category"},"contents":{"items":{"$ref":"#/components/schemas/BucketTask"},"type":"array","title":"Contents"},"eligibility":{"type":"string","enum":["conditional","unconditional","ineligible"],"title":"Eligibility","examples":["conditional"]},"missing_questions":{"items":{"type":"string"},"type":"array","title":"Missing Questions","examples":[["fb20fd4773304500b39c4f6de0012a5a"]]},"loi":{"type":"integer","title":"Loi","description":"this is the max loi of the contents","examples":[612]},"payout":{"type":"integer","title":"Payout","description":"this is the min payout of the contents","examples":[123]}},"additionalProperties":false,"type":"object","required":["id","uri","contents","eligibility","loi","payout"],"title":"SoftPairBucket"},"SoftPairOfferwall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/SoftPairBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]},"question_info":{"additionalProperties":{"$ref":"#/components/schemas/UpkQuestion"},"type":"object","title":"Question Info","examples":[]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"SoftPairOfferwall","description":"This offerwall contains tasks for which the user has a conditional\neligibility. The questions that a user must answer to determine the\neligibility are included within each bucket. Additionally, the question\ndefinitions are included for convenience.\n\nOfferwall code: `37d1da64`"},"SoftPairOfferwallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/SoftPairOfferwall"}},"type":"object","required":["info","offerwall"],"title":"SoftPairOfferwallResponse"},"Source":{"type":"string","enum":["g","c","a","d","et","f","i","l","m","n","o","e","r","pr","p","rd","h","s","t","u","w"],"title":"Source"},"SourceConfig":{"properties":{"name":{"$ref":"#/components/schemas/Source"},"active":{"type":"boolean","title":"Active","default":true},"banned_countries":{"items":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},"type":"array","title":"Banned Countries"},"allow_mobile_ip":{"type":"boolean","title":"Allow Mobile Ip","default":true},"allow_pii_only_buyers":{"type":"boolean","title":"Allow Pii Only Buyers","description":"Allow Tasks from Buyers that want traffic that comes from Suppliers that can identify their users. Only supported on Pure Spectrum.","default":false},"allow_unhashed_buyers":{"type":"boolean","title":"Allow Unhashed Buyers","description":"Return Tasks from Buyers that don't have URL hashing enabled. Only supported on Pure Spectrum.","default":false},"withhold_profiling":{"type":"boolean","title":"Withhold Profiling","description":"For some Products, we may have privacy agreements prohibiting us from sharing information with the inventorySource. If True, don't add MRPQ (Market Research ProfilingQuestion) onto the entry link.","default":false},"pass_unconditional_eligible_unknowns":{"type":"boolean","title":"Pass Unconditional Eligible Unknowns","description":"Not used at the moment","default":true}},"type":"object","required":["name"],"title":"SourceConfig","description":"This could also be named \"BP:Marketplace Config\", as it describes the\nconfig for a BP on a single marketplace."},"SourcesConfig":{"properties":{"user_defined":{"items":{"$ref":"#/components/schemas/SourceConfig"},"type":"array","title":"User Defined"}},"type":"object","title":"SourcesConfig","description":"Describes the marketplaces or sources that a BP can access and their\nrespective configs,\naka 'BP:Marketplace Configs'"},"StarwallPlusBlockOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"StarwallPlusBlockOfferWall","description":"Same as the StarwallOfferWall, but the buckets include contents and no\nbuckets are returned if the user is blocked.\n\nOfferwall code: `7fa1b3f4`"},"StarwallPlusBlockOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/StarwallPlusBlockOfferWall"}},"type":"object","required":["info","offerwall"],"title":"StarwallPlusBlockOfferWallResponse"},"StarwallPlusBlockRecontactOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusRecontactBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"StarwallPlusBlockRecontactOfferWall","description":"Same as the StarwallOfferWall, but the buckets include contents, no\nbuckets are returned if the user is blocked, and each bucket includes\na recontact key.\n\nOfferwall code: `630db2a4`"},"StarwallPlusBlockRecontactOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/StarwallPlusBlockRecontactOfferWall"}},"type":"object","required":["info","offerwall"],"title":"StarwallPlusBlockRecontactOfferWallResponse"},"StarwallPlusOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"StarwallPlusOfferWall","description":"Same as the StarwallOfferWall, but the buckets include contents.\n\nOfferwall code: `5481f322`"},"StarwallPlusOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/StarwallPlusOfferWall"}},"type":"object","required":["info","offerwall"],"title":"StarwallPlusOfferWallResponse"},"StatisticalSummaryStat":{"properties":{"facet":{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"}]},"propertyNames":{"enum":["country_iso","day","month"]},"type":"object","title":"Facet","description":"The grouping criteria","examples":[{"country_iso":"us"}]},"value":{"$ref":"#/components/schemas/StatisticalSummaryValue","description":"Statistical Summary for the given metric and facet"}},"type":"object","required":["facet","value"],"title":"StatisticalSummaryStat"},"StatisticalSummaryValue":{"properties":{"min":{"type":"number","title":"Min"},"max":{"type":"number","title":"Max"},"mean":{"type":"number","title":"Mean"},"q1":{"type":"number","title":"Q1"},"q2":{"type":"number","title":"Q2","description":"equal to the median"},"q3":{"type":"number","title":"Q3"},"lower_whisker":{"type":"number","title":"Lower Whisker","readOnly":true},"upper_whisker":{"type":"number","title":"Upper Whisker","readOnly":true}},"type":"object","required":["min","max","mean","q1","q2","q3","lower_whisker","upper_whisker"],"title":"StatisticalSummaryValue"},"StatsMessage":{"properties":{"kind":{"type":"string","const":"stats","title":"Kind","default":"stats"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"},"country_iso":{"anyOf":[{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},{"type":"null"}],"title":"Country Iso"},"data":{"$ref":"#/components/schemas/StatsSnapshot"}},"type":"object","required":["data"],"title":"StatsMessage"},"StatsSnapshot":{"properties":{"live_task_count":{"$ref":"#/components/schemas/AggregateBySource"},"task_created_count_last_1h":{"$ref":"#/components/schemas/AggregateBySource"},"task_created_count_last_24h":{"$ref":"#/components/schemas/AggregateBySource"},"live_tasks_max_payout":{"$ref":"#/components/schemas/MaxGaugeBySource","description":"In integer USDCents"},"country_iso":{"anyOf":[{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},{"type":"null"}],"title":"Country Iso"},"timestamp":{"type":"string","format":"date-time","title":"Timestamp"},"active_users_last_1h":{"type":"integer","minimum":0.0,"title":"Active Users Last 1H","description":"Count of users (in this product_id) that were active in the past 60 minutes.\n Behaviors that trigger active:\n - Request an offerwall\n - Enter an offerwall bucket\n - Request profiling questions\n - Submit profiling answers\n - Update user profile\n ","default":0},"active_users_last_24h":{"type":"integer","minimum":0.0,"title":"Active Users Last 24H","description":"Count of users (in this product_id) that were active in the past 24 hours.","default":0},"in_progress_users":{"type":"integer","minimum":0.0,"title":"In Progress Users","description":"Count of users that are currently doing work at this moment"},"signups_last_24h":{"type":"integer","minimum":0.0,"title":"Signups Last 24H","description":"Count of users created"},"session_enters_last_1h":{"type":"integer","minimum":0.0,"title":"Session Enters Last 1H"},"session_enters_last_24h":{"type":"integer","minimum":0.0,"title":"Session Enters Last 24H"},"session_fails_last_1h":{"type":"integer","minimum":0.0,"title":"Session Fails Last 1H"},"session_fails_last_24h":{"type":"integer","minimum":0.0,"title":"Session Fails Last 24H"},"session_completes_last_1h":{"type":"integer","minimum":0.0,"title":"Session Completes Last 1H"},"session_completes_last_24h":{"type":"integer","minimum":0.0,"title":"Session Completes Last 24H"},"sum_payouts_last_1h":{"type":"integer","minimum":0.0,"title":"Sum Payouts Last 1H","description":"In integer USDCents"},"sum_payouts_last_24h":{"type":"integer","minimum":0.0,"title":"Sum Payouts Last 24H","description":"In integer USDCents"},"session_avg_payout_last_24h":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Session Avg Payout Last 24H","description":"Average (actual) payout of all tasks completed in the past 24 hrs"},"session_avg_user_payout_last_24h":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Session Avg User Payout Last 24H","description":"Average (actual) user payout of all tasks completed in the past 24 hrs"},"session_fail_avg_loi_last_24h":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Session Fail Avg Loi Last 24H","description":"Average LOI of all tasks terminated in the past 24 hrs (excludes abandons)"},"session_complete_avg_loi_last_24h":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Session Complete Avg Loi Last 24H","description":"Average LOI of all tasks completed in the past 24 hrs"}},"type":"object","required":["in_progress_users","signups_last_24h","session_enters_last_1h","session_enters_last_24h","session_fails_last_1h","session_fails_last_24h","session_completes_last_1h","session_completes_last_24h","sum_payouts_last_1h","sum_payouts_last_24h","session_avg_payout_last_24h","session_avg_user_payout_last_24h","session_fail_avg_loi_last_24h","session_complete_avg_loi_last_24h"],"title":"StatsSnapshot"},"Status":{"type":"string","enum":["c","f","a","t"],"title":"Status","description":"The outcome of a session or wall event. If the session is still in\nprogress, the status will be NULL."},"StatusCode1":{"type":"integer","enum":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19],"title":"StatusCode1","description":"__High level status code for outcome of the session.__\nThis should only be NULL if the Status is ABANDON or TIMEOUT"},"StatusInfoResponseFail":{"properties":{"info":{"$ref":"#/components/schemas/StatusSuccessFail"},"msg":{"type":"string","title":"Msg","description":"An optional message, if success is False","examples":["An error has occurred"]}},"type":"object","required":["info","msg"],"title":"StatusInfoResponseFail"},"StatusResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]}},"type":"object","required":["status"],"title":"StatusResponse"},"StatusResponseFailure":{"properties":{"status":{"type":"string","const":"failure","title":"Status","description":"The status of the API response.","examples":["failure"]},"msg":{"type":"string","title":"Msg","description":"An optional message, if the status is failure.","examples":["An error has occurred"]}},"type":"object","required":["status","msg"],"title":"StatusResponseFailure"},"StatusSuccessFail":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether the API response is successful.","default":false}},"type":"object","title":"StatusSuccessFail"},"StreakFulfillment":{"type":"string","enum":["active","complete"],"title":"StreakFulfillment","description":"What has to happen for a user to fulfill a period for a streak"},"StreakPeriod":{"type":"string","enum":["day","week","month"],"title":"StreakPeriod"},"StreakState":{"type":"string","enum":["active","at_risk","broken"],"title":"StreakState"},"SubscribeMessage":{"properties":{"kind":{"type":"string","const":"subscribe","title":"Kind","default":"subscribe"},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","examples":["4fe381fb7186416cb443a38fa66c6557"]}},"type":"object","required":["product_id"],"title":"SubscribeMessage"},"SupplierTag":{"type":"string","enum":["mobile","js-offerwall","double-opt-in","single-sign-on","phone-number-verified","test-a","test-b"],"title":"SupplierTag","description":"Available tags which can be used to annotate supplier traffic\n\nNote: should not include commas!"},"SupplyConfig":{"properties":{"policies":{"items":{"$ref":"#/components/schemas/SupplyPolicy"},"type":"array","title":"Policies"}},"type":"object","title":"SupplyConfig","description":"Describes the set of policies for how GRL can interact with marketplaces.\nThis is only used on the special \"global product\"."},"SupplyPolicy":{"properties":{"name":{"$ref":"#/components/schemas/Source"},"active":{"type":"boolean","title":"Active","default":true},"banned_countries":{"items":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},"type":"array","title":"Banned Countries"},"allow_mobile_ip":{"type":"boolean","title":"Allow Mobile Ip","default":true},"allow_pii_only_buyers":{"type":"boolean","title":"Allow Pii Only Buyers","description":"Allow Tasks from Buyers that want traffic that comes from Suppliers that can identify their users. Only supported on Pure Spectrum.","default":false},"allow_unhashed_buyers":{"type":"boolean","title":"Allow Unhashed Buyers","description":"Return Tasks from Buyers that don't have URL hashing enabled. Only supported on Pure Spectrum.","default":false},"withhold_profiling":{"type":"boolean","title":"Withhold Profiling","description":"For some Products, we may have privacy agreements prohibiting us from sharing information with the inventorySource. If True, don't add MRPQ (Market Research ProfilingQuestion) onto the entry link.","default":false},"pass_unconditional_eligible_unknowns":{"type":"boolean","title":"Pass Unconditional Eligible Unknowns","description":"Not used at the moment","default":true},"address":{"items":{"type":"string"},"type":"array","title":"Address","description":"address for the grpc GetOpps call"},"allow_vpn":{"type":"boolean","title":"Allow Vpn","default":false},"distribute_harmonizer_active":{"type":"boolean","title":"Distribute Harmonizer Active","default":true},"supplier_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Supplier Id","description":"For some inventory Sources, we may partition traffic using different supplier accounts instead"},"team_ids":{"anyOf":[{"items":{"type":"string","maxLength":32,"minLength":32},"type":"array"},{"type":"null"}],"title":"Team Ids"},"product_ids":{"anyOf":[{"items":{"type":"string","maxLength":32,"minLength":32},"type":"array"},{"type":"null"}],"title":"Product Ids"},"integration_mode":{"$ref":"#/components/schemas/IntegrationMode","default":"platform"},"scope":{"$ref":"#/components/schemas/Scope","description":"There must be only 1 GLOBAL config per Source. We can have more than one TEAM/PRODUCT config per Source.","readOnly":true}},"type":"object","required":["name","address","scope"],"title":"SupplyPolicy","description":"One policy describing how GRL can interact with a marketplaces in a\ncertain way. This is only used on the special \"global product\", and then\ninternally in grpc logic."},"SurveyEligibilityCriterion":{"properties":{"question_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Question Id","examples":["71a367fb71b243dc89f0012e0ec91749"]},"property_code":{"anyOf":[{"type":"string","maxLength":64,"minLength":3,"pattern":"^[a-z]{1,2}\\:.*"},{"type":"null"}],"title":"Property Code","examples":["c:73629"]},"question_text":{"type":"string","title":"Question Text","examples":["What company administers the retirement plan for your current employer?"]},"qualifying_answer":{"items":{"type":"string"},"type":"array","title":"Qualifying Answer","description":"User answer(s) that satisfied at least one eligibility rule","examples":["121"]},"qualifying_answer_label":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Qualifying Answer Label","examples":["Fidelity Investments"]},"explanation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation","description":"Human-readable text explaining how a user's answer to this question affects eligibility","examples":["The company that administers your employer's retirement plan is **Fidelity Investments**."]},"rank":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Rank","description":"Lower values are shown more prominently in the UI"}},"type":"object","required":["question_id","property_code","question_text","qualifying_answer","qualifying_answer_label"],"title":"SurveyEligibilityCriterion","description":"Explanatory record of which question answers contributed\n to a user's eligibility for a survey.\nThis is INSUFFICIENT for determining eligibility to a task\n as it IGNORES logical operators, dependencies between criteria,\n and other requirements. It is only intended for the UI."},"TangoCashoutMethodData":{"properties":{"type":{"type":"string","const":"TANGO","title":"Type","default":"TANGO"},"utid":{"type":"string","title":"Utid","description":"tango utid"},"countries":{"items":{"type":"string"},"type":"array","title":"Countries"},"value_type":{"type":"string","enum":["variable","fixed"],"title":"Value Type"},"disclaimer":{"type":"string","title":"Disclaimer","default":""},"terms":{"type":"string","title":"Terms","default":""}},"type":"object","required":["utid","countries","value_type"],"title":"TangoCashoutMethodData"},"TaskEnterPayload":{"properties":{"event_type":{"type":"string","const":"task.enter","title":"Event Type","default":"task.enter"},"source":{"$ref":"#/components/schemas/Source"},"survey_id":{"type":"string","maxLength":32,"minLength":1,"title":"Survey Id","examples":["127492892"]},"quota_id":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Quota Id","description":"The marketplace's internal quota id"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"}},"type":"object","required":["source","survey_id","country_iso"],"title":"TaskEnterPayload"},"TaskFinishPayload":{"properties":{"event_type":{"type":"string","const":"task.finish","title":"Event Type","default":"task.finish"},"source":{"$ref":"#/components/schemas/Source"},"survey_id":{"type":"string","maxLength":32,"minLength":1,"title":"Survey Id","examples":["127492892"]},"quota_id":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Quota Id","description":"The marketplace's internal quota id"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"},"duration_sec":{"type":"number","exclusiveMinimum":0.0,"title":"Duration Sec"},"status":{"$ref":"#/components/schemas/Status"},"status_code_1":{"anyOf":[{"$ref":"#/components/schemas/StatusCode1"},{"type":"null"}]},"status_code_2":{"anyOf":[{"$ref":"#/components/schemas/WallStatusCode2"},{"type":"null"}]},"cpi":{"anyOf":[{"type":"integer","maximum":4000.0,"minimum":0.0},{"type":"null"}],"title":"Cpi"}},"type":"object","required":["source","survey_id","country_iso","duration_sec","status"],"title":"TaskFinishPayload"},"TaskStatusResponse":{"properties":{"tsid":{"type":"string","maxLength":32,"minLength":32,"title":"Tsid","description":"A unique identifier for the session","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"product_id":{"type":"string","maxLength":32,"minLength":32,"title":"Product Id","description":"The BP ID of the associated respondent","examples":["1188cb21cb6741d79f614f6d02e9bc2a"]},"product_user_id":{"type":"string","maxLength":128,"minLength":3,"title":"Product User Id","description":"A unique identifier for each user, which is set by the Supplier","examples":["app-user-9329ebd"]},"started":{"type":"string","format":"date-time","title":"Started","description":"When the session was started"},"finished":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Finished","description":"When the session was finished"},"status":{"type":"integer","description":"The outcome of a session.\n - 0 - UNKNOWN\n - 1 - ENTER (the user has not yet returned)\n - 2 - INCOMPLETE (the user failed)\n - 3 - COMPLETE (the user completed the task)","examples":[3]},"payout":{"anyOf":[{"type":"integer","exclusiveMaximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Payout","description":"The amount paid to the supplier, in integer USD cents","examples":[500]},"user_payout":{"anyOf":[{"type":"integer","exclusiveMaximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"User Payout","description":"If a payout transformation is configured on this account, this is the amount the user should earn, in integer USD cents","examples":[337]},"payout_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payout Format","description":"\nThe format describing the str representation of a payout. Typically, this would be displayed to a user.\nThe payout_format is similar to python format string with a subset of functionality supported.\nOnly float with a precision are supported along with an optional comma for a thousands separator.\nIn addition, a mathematical operator can be applied, such as dividing by 100. \nExamples are shown assuming payout = 100 (one dollar).\n- \"{payout*10:,.0f} Points\" -> \"1,000 Points\"\n- \"${payout/100:.2f}\" -> \"$1.00\"\n","examples":["{payout*10:,.0f} Points","${payout/100:.2f}","{payout:.0f}"]},"user_payout_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Payout String","description":"If a payout transformation is configured on this account, this is the amount to display to the user","examples":["3370 Points"]},"kwargs":{"additionalProperties":{"type":"string"},"type":"object","title":"Kwargs","description":"Any extra url params used in the offerwall request will be passed back here"},"status_code_1":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"__High level status code for outcome of the session.__\nThis should only be NULL if the Status is ABANDON or TIMEOUT\n\nAllowed values: \n - __BUYER_FAIL__: User terminated in buyer survey\n - __BUYER_QUALITY_FAIL__: User terminated in buyer survey for quality reasons\n - __PS_FAIL__: User failed in marketplace's prescreener\n - __PS_QUALITY__: User rejected by marketplace for quality reasons\n - __PS_BLOCKED__: User is explicitly blocked by the marketplace. Note: on some marketplaces, users can have multiple PS_QUALITY terminations and still complete surveys.\n - __PS_OVERQUOTA__: User rejected by marketplace for over quota\n - __PS_DUPLICATE__: User rejected by marketplace for duplicate\n - __GRS_FAIL__: The user failed within the GRS Platform\n - __GRS_QUALITY_FAIL__: The user failed within the GRS Platform for quality reasons\n - __GRS_ABANDON__: The user abandoned/timed out within the GRS Platform\n - __PS_ABANDON__: The user abandoned/timed out within the marketplace's pre-screen system. Note: On most marketplaces, we have no way of distinguishing between this and BUYER_ABANDON. BUYER_ABANDON is used as the default, unless we know it is PS_ABANDON.\n - __BUYER_ABANDON__: The user abandoned/timed out within the client survey\n - __UNKNOWN__: The status code is not documented\n - __COMPLETE__: The user completed the task successfully\n - __MARKETPLACE_FAIL__: Something was wrong upon the user redirecting from the marketplace, e.g. no postback received, or url hashing failures.\n - __SESSION_START_FAIL__: User failed before being sent into a marketplace\n - __SESSION_CONTINUE_FAIL__: User failed between attempts\n - __SESSION_START_QUALITY_FAIL__: User failed before being sent into a marketplace for \"security\" reasons\n - __SESSION_CONTINUE_QUALITY_FAIL__: User failed between attempts for \"security\" reasons","examples":["COMPLETE"]},"status_code_2":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"__Status Detail__\nThis should be set if the Session.status_code_1 is SESSION_XXX_FAIL\n\nAllowed values: \n - __ENTRY_URL_MODIFICATION__: Unable to parse either the bucket_id, request_id, or nudge_id from the url\n - __UNRECOGNIZED_IP__: The client's IP failed GRIP lookup, or we failed to store it for some reason\n - __USER_IS_ANONYMOUS__: User is using an anonymous IP\n - __USER_IS_BLOCKED__: User is blocked\n - __USER_IS_RATE_LIMITED__: User is rate limited\n - __UNRECOGNIZED_DEVICE__: The client's useragent was not categorized as desktop, mobile, or tablet\n - __OFFERWALL_EXPIRED__: The user clicked after 5 min\n - __INTERNAL_ERROR__: Something unexpected happened\n - __OFFERWALL_COUNTRY_MISMATCH__: The user requested the offerwall for a different country than their IP address indicates\n - __INVALID_BUCKET_ID__: The bucket id indicated in the url does not exist. This is likely due to the user clicking on a bucket for an offerwall that has already been refreshed.\n - __NO_TASKS_AVAILABLE__: Not necessarily the user's fault. We thought we had surveys, but due to for e.g. the user entering on a different device than we thought, there really are none. If we get a lot of these, then that might indicate something is wrong.\n - __ATTEMPT_IS_SUSPICIOUS__: The entrance attempt was flagged by GRLIQ as suspicious\n - __GRLIQ_MISSING__: No GRLIQ forensics post was received","examples":[null]},"adjusted_status":{"anyOf":[{"$ref":"#/components/schemas/SessionAdjustedStatus"},{"type":"null"}],"description":"An adjusted_status is set if a session is adjusted by the marketplace\nafter the original return. A session can be adjusted multiple times.\nThis is the most recent status. If a session was originally a complete,\nwas adjusted to incomplete, then back to complete, the adjusted_status\nwill be None, but the adjusted_timestamp will be set to the most recent\nchange.\n\nAllowed values: \n - __ac__ *(ADJUSTED_TO_COMPLETE)*: Task was reconciled to complete\n - __af__ *(ADJUSTED_TO_FAIL)*: Task was reconciled to incomplete\n - __pa__ *(PAYOUT_ADJUSTMENT)*: The payout was changed. This applies to Sessions ONLY.","examples":[null]},"adjusted_timestamp":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Adjusted Timestamp","description":"When the adjusted status was last set.","examples":[null]},"adjusted_payout":{"anyOf":[{"type":"integer","exclusiveMaximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Adjusted Payout","description":"The new payout after adjustment.","examples":[null]},"adjusted_user_payout":{"anyOf":[{"type":"integer","exclusiveMaximum":100000.0,"minimum":0.0},{"type":"null"}],"title":"Adjusted User Payout","description":"The new user_payout after adjustment.","examples":[null]},"adjusted_user_payout_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Adjusted User Payout String","description":"The new user_payout_string after adjustment.","examples":[null]},"wall_events":{"anyOf":[{"items":{"$ref":"#/components/schemas/WallOut"},"type":"array"},{"type":"null"}],"title":"Wall Events"},"currency":{"type":"string","const":"USD","title":"Currency","default":"USD"},"final_status":{"type":"integer","title":"Final Status","description":"This is deprecated","default":0},"bpuid":{"type":"string","title":"Bpuid","readOnly":true}},"type":"object","required":["tsid","product_id","product_user_id","started","bpuid"],"title":"TaskStatusResponse","description":"The status of a session"},"TasksStatusResponse":{"properties":{"page":{"type":"integer","minimum":1.0,"title":"Page","description":"Page number","default":1},"size":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Size","description":"Page size","default":50},"total":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Total","description":"Total number of results"},"tasks_status":{"items":{"$ref":"#/components/schemas/TaskStatusResponse"},"type":"array","title":"Tasks Status"},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages","description":"Total number of pages","readOnly":true}},"type":"object","required":["pages"],"title":"TasksStatusResponse"},"TopNPlusBlockOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"TopNPlusBlockOfferWall","description":"Same as the TopNOfferWall, but the buckets include contents and no\nbuckets are returned if the user is blocked.\n\nOfferwall code: `d48cce47`"},"TopNPlusBlockOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/TopNPlusBlockOfferWall"}},"type":"object","required":["info","offerwall"],"title":"TopNPlusBlockOfferWallResponse"},"TopNPlusBlockRecontactOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusRecontactBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"TopNPlusBlockRecontactOfferWall","description":"Same as the TopNOfferWall, but the buckets include contents, no buckets\nare returned if the user is blocked, and each bucket includes a\n`is_recontact` key.\n\nOfferwall code: `1e5f0af8`"},"TopNPlusBlockRecontactOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/TopNPlusBlockRecontactOfferWall"}},"type":"object","required":["info","offerwall"],"title":"TopNPlusBlockRecontactOfferWallResponse"},"TopNPlusBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"x":{"type":"integer","minimum":0.0,"title":"X","description":"For UI. Provides a dimensionality position for the bucket on the x-axis.","default":0,"examples":[0,1,2]},"y":{"type":"integer","minimum":0.0,"title":"Y","description":"For UI. Provides a dimensionality position for the bucket on the y-axis.","default":0,"examples":[0,1,2]},"name":{"type":"string","title":"Name","description":"Currently unused. Will always return empty string","default":""},"description":{"type":"string","title":"Description","description":"Currently unused. Will always return empty string","default":""},"category":{"items":{"$ref":"#/components/schemas/CategoryAssociation"},"type":"array","title":"Category"},"contents":{"items":{"$ref":"#/components/schemas/BucketTask"},"type":"array","title":"Contents"},"duration":{"$ref":"#/components/schemas/DurationSummary"},"payout":{"$ref":"#/components/schemas/PayoutSummary"},"quality_score":{"type":"number","title":"Quality Score"},"currency":{"type":"string","title":"Currency","description":"This will always be 'USD'","default":"USD","examples":["USD"]},"eligibility_criteria":{"items":{"$ref":"#/components/schemas/SurveyEligibilityCriterion"},"type":"array","title":"Eligibility Criteria","description":"The reasons the user is eligible for tasks in this bucket"},"eligibility_explanation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Eligibility Explanation","description":"Human-readable text explaining a user's eligibility for tasks in this bucket","examples":["You are a **47-year-old** **white** **male** with a *college degree*, who's employer's retirement plan is **Fidelity Investments**."]}},"additionalProperties":false,"type":"object","required":["id","uri","contents","duration","payout","quality_score"],"title":"TopNPlusBucket"},"TopNPlusOfferWall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/TopNPlusBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"TopNPlusOfferWall","description":"Same as the TopNOfferWall, but the buckets include contents.\n\nOfferwall code: `b145b803`"},"TopNPlusOfferWallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/TopNPlusOfferWall"}},"type":"object","required":["info","offerwall"],"title":"TopNPlusOfferWallResponse"},"TopNPlusRecontactBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"x":{"type":"integer","minimum":0.0,"title":"X","description":"For UI. Provides a dimensionality position for the bucket on the x-axis.","default":0,"examples":[0,1,2]},"y":{"type":"integer","minimum":0.0,"title":"Y","description":"For UI. Provides a dimensionality position for the bucket on the y-axis.","default":0,"examples":[0,1,2]},"name":{"type":"string","title":"Name","description":"Currently unused. Will always return empty string","default":""},"description":{"type":"string","title":"Description","description":"Currently unused. Will always return empty string","default":""},"category":{"items":{"$ref":"#/components/schemas/CategoryAssociation"},"type":"array","title":"Category"},"contents":{"items":{"$ref":"#/components/schemas/BucketTask"},"type":"array","title":"Contents"},"duration":{"$ref":"#/components/schemas/DurationSummary"},"payout":{"$ref":"#/components/schemas/PayoutSummary"},"quality_score":{"type":"number","title":"Quality Score"},"is_recontact":{"type":"boolean","title":"Is Recontact"},"currency":{"type":"string","title":"Currency","description":"This will always be 'USD'","default":"USD","examples":["USD"]}},"additionalProperties":false,"type":"object","required":["id","uri","contents","duration","payout","quality_score","is_recontact"],"title":"TopNPlusRecontactBucket"},"UPKImportance":{"properties":{"task_count":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Task Count","description":"The number of live Tasks that use this UPK Question","examples":[47]},"task_score":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Task Score","description":"GRL's internal ranked score for the UPK Question","examples":[0.11175522477414712]},"marketplace_task_count":{"anyOf":[{"additionalProperties":{"type":"integer","minimum":0.0},"propertyNames":{"$ref":"#/components/schemas/Source"},"type":"object"},{"type":"null"}],"title":"Marketplace Task Count","description":"The number of live Tasks that use this UPK Question per marketplace","examples":[{"d":23,"s":24}]}},"type":"object","title":"UPKImportance"},"USDeliveryAddress":{"properties":{"name_or_attn":{"type":"string","maxLength":50,"minLength":1,"title":"Name Or Attn"},"company":{"anyOf":[{"type":"string","maxLength":50,"minLength":1},{"type":"null"}],"title":"Company"},"phone_number":{"anyOf":[{"type":"string","maxLength":10,"minLength":10,"pattern":"^[0-9]+$"},{"type":"null"}],"title":"Phone Number"},"address":{"type":"string","maxLength":100,"minLength":1,"title":"Address"},"city":{"type":"string","maxLength":100,"minLength":1,"title":"City"},"state":{"type":"string","maxLength":2,"minLength":1,"title":"State"},"postal_code":{"type":"string","maxLength":10,"minLength":1,"title":"Postal Code"},"country":{"type":"string","title":"Country","default":"us"}},"type":"object","required":["name_or_attn","address","city","state","postal_code"],"title":"USDeliveryAddress"},"UpkItem":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","examples":["497b1fedec464151b063cd5367643ffa"]},"label":{"type":"string","maxLength":255,"title":"Label","examples":["high_school_completion"]},"description":{"anyOf":[{"type":"string","maxLength":1024},{"type":"null"}],"title":"Description","examples":["Completed high school"]}},"type":"object","required":["id","label"],"title":"UpkItem"},"UpkProperty":{"properties":{"property_id":{"type":"string","maxLength":32,"minLength":32,"title":"Property Id","examples":["7a0635061fa64c98b43c6a3d15459f36"]},"property_label":{"type":"string","maxLength":255,"title":"Property Label","examples":["educational_attainment"]},"prop_type":{"$ref":"#/components/schemas/PropertyType","description":"\nAllowed values: \n - __i__ *(UPK_ITEM)*: UserProfileKnowledge Item\n - __n__ *(UPK_NUMERICAL)*: UserProfileKnowledge Numerical\n - __x__ *(UPK_TEXT)*: UserProfileKnowledge Text","default":"i"},"cardinality":{"$ref":"#/components/schemas/Cardinality","description":"\nAllowed values: \n - __*__ *(ZERO_OR_MORE)*: Zero or More\n - __?__ *(ZERO_OR_ONE)*: Zero or One","default":"?"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"},"gold_standard":{"type":"boolean","title":"Gold Standard","description":"A Gold-Standard question has been enumerated for all possible values (per country) as best as possible by GRL,allowing it to be mapped across inventory sources. A property not marked as Gold-Standard may have: 1) marketplace qid associations & 2) category associations, but doesn't have a defined 'range' (list of allowed itemsin a multiple choice question). This is used for exposing a user's profiling data & forthe Nudge API.","default":false},"allowed_items":{"anyOf":[{"items":{"$ref":"#/components/schemas/UpkItem"},"type":"array"},{"type":"null"}],"title":"Allowed Items"},"categories":{"items":{"$ref":"#/components/schemas/Category"},"type":"array","title":"Categories"}},"type":"object","required":["property_id","property_label","country_iso"],"title":"UpkProperty","description":"This used to be called \"QuestionInfo\", which is a bad name,\n as this describes a UPK Property, like \"educational_attainment\",\n not the question that asks for your education."},"UpkQuestion":{"properties":{"question_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Question Id"},"ext_question_id":{"anyOf":[{"type":"string","pattern":"^[a-z]{1,2}\\:.*"},{"type":"null"}],"title":"Ext Question Id","description":"what marketplace question this question links to"},"question_type":{"$ref":"#/components/schemas/UpkQuestionType"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"},"language_iso":{"type":"string","maxLength":3,"minLength":3,"pattern":"^[a-z]{3}$","title":"Language Iso"},"question_text":{"type":"string","minLength":1,"title":"Question Text","description":"The text shown to respondents"},"choices":{"anyOf":[{"items":{"$ref":"#/components/schemas/UpkQuestionChoice"},"type":"array"},{"type":"null"}],"title":"Choices"},"selector":{"anyOf":[{"$ref":"#/components/schemas/UpkQuestionSelectorMC"},{"$ref":"#/components/schemas/UpkQuestionSelectorTE"},{"$ref":"#/components/schemas/UpkQuestionSelectorSLIDER"},{"$ref":"#/components/schemas/UpkQuestionSelectorHIDDEN"}],"title":"Selector"},"configuration":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/UpkQuestionConfigurationMC"},{"$ref":"#/components/schemas/UpkQuestionConfigurationTE"},{"$ref":"#/components/schemas/UpkQuestionConfigurationSLIDER"}],"discriminator":{"propertyName":"type","mapping":{"MC":"#/components/schemas/UpkQuestionConfigurationMC","SLIDER":"#/components/schemas/UpkQuestionConfigurationSLIDER","TE":"#/components/schemas/UpkQuestionConfigurationTE"}}},{"type":"null"}],"title":"Configuration"},"validation":{"anyOf":[{"$ref":"#/components/schemas/UpkQuestionValidation"},{"type":"null"}]},"importance":{"anyOf":[{"$ref":"#/components/schemas/UPKImportance"},{"type":"null"}]},"categories":{"items":{"$ref":"#/components/schemas/Category"},"type":"array","title":"Categories","description":"Categories associated with this question"},"explanation_template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation Template","description":"Human-readable template for explaining how a user's answer to this question affects eligibility","examples":["The company that administers your employer's retirement plan is {answer}."]},"explanation_fragment_template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation Fragment Template","description":"A very short, natural-language explanation fragment that can be combined with others into a single sentence","examples":["whose employer's retirement plan is {answer}"]}},"type":"object","required":["question_type","country_iso","language_iso","question_text","selector"],"title":"UpkQuestion","example":{"categories":[{"adwords_vertical_id":"396","label":"Politics","parent_uuid":"f66dddba61424ce5be2a38731450a0e1","path":"/News/Politics","uuid":"87b6d819f3ca4815bf1f135b1e829cc6"}],"choices":[{"choice_id":"1","choice_text":"Yes","exclusive":false,"order":0},{"choice_id":"2","choice_text":"No","exclusive":false,"order":1}],"country_iso":"us","importance":{"marketplace_task_count":{"d":9,"s":20,"w":20},"task_count":49,"task_score":3.3401743283265684},"language_iso":"eng","marketplace_task_count":{"d":9,"s":20,"w":20},"question_id":"fb20fd4773304500b39c4f6de0012a5a","question_text":"Are you registered to vote at your present address, or not?","question_type":"MC","selector":"SA","task_count":49,"task_score":3.3401743283265684}},"UpkQuestionChoice":{"properties":{"choice_id":{"type":"string","maxLength":64,"minLength":1,"pattern":"^[\\w\\s\\.\\-]+$","title":"Choice Id","description":"The unique identifier for a response to a qualification"},"choice_text":{"type":"string","minLength":1,"title":"Choice Text","description":"The response text shown to respondents"},"order":{"type":"integer","minimum":0.0,"title":"Order"},"group":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Group"},"exclusive":{"type":"boolean","title":"Exclusive","description":"If answer is exclusive, it can be the only option selected","default":false},"importance":{"anyOf":[{"$ref":"#/components/schemas/UPKImportance"},{"type":"null"}]}},"type":"object","required":["choice_id","choice_text","order"],"title":"UpkQuestionChoice"},"UpkQuestionChoiceOut":{"properties":{"choice_id":{"type":"string","maxLength":64,"minLength":1,"pattern":"^[\\w\\s\\.\\-]+$","title":"Choice Id","description":"The unique identifier for a response to a qualification"},"choice_text":{"type":"string","minLength":1,"title":"Choice Text","description":"The response text shown to respondents"},"order":{"type":"integer","minimum":0.0,"title":"Order"},"group":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Group"},"exclusive":{"type":"boolean","title":"Exclusive","description":"If answer is exclusive, it can be the only option selected","default":false},"importance":{"anyOf":[{"$ref":"#/components/schemas/UPKImportance"},{"type":"null"}]}},"type":"object","required":["choice_id","choice_text","order"],"title":"UpkQuestionChoiceOut"},"UpkQuestionConfigurationMC":{"properties":{"max_select":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Max Select"}},"additionalProperties":false,"type":"object","title":"UpkQuestionConfigurationMC"},"UpkQuestionConfigurationSLIDER":{"properties":{"slider_min":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Slider Min"},"slider_max":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Slider Max"},"slider_start":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Slider Start"},"slider_step":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Slider Step"}},"type":"object","title":"UpkQuestionConfigurationSLIDER"},"UpkQuestionConfigurationTE":{"properties":{"max_length":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Max Length","description":"Maximum str length of any input. Meant as an easy, nonregex based check."},"min_length":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Min Length","description":"Minimum str length of any input. Meant as an easy, nonregex based check."}},"additionalProperties":false,"type":"object","title":"UpkQuestionConfigurationTE"},"UpkQuestionOut":{"properties":{"question_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Question Id"},"ext_question_id":{"anyOf":[{"type":"string","pattern":"^[a-z]{1,2}\\:.*"},{"type":"null"}],"title":"Ext Question Id","description":"what marketplace question this question links to"},"question_type":{"$ref":"#/components/schemas/UpkQuestionType"},"country_iso":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$","title":"Country Iso"},"language_iso":{"type":"string","maxLength":3,"minLength":3,"pattern":"^[a-z]{3}$","title":"Language Iso"},"question_text":{"type":"string","minLength":1,"title":"Question Text","description":"The text shown to respondents"},"choices":{"anyOf":[{"items":{"$ref":"#/components/schemas/UpkQuestionChoiceOut"},"type":"array"},{"type":"null"}],"title":"Choices"},"selector":{"anyOf":[{"$ref":"#/components/schemas/UpkQuestionSelectorMC"},{"$ref":"#/components/schemas/UpkQuestionSelectorTE"},{"$ref":"#/components/schemas/UpkQuestionSelectorSLIDER"},{"$ref":"#/components/schemas/UpkQuestionSelectorHIDDEN"}],"title":"Selector"},"configuration":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/UpkQuestionConfigurationMC"},{"$ref":"#/components/schemas/UpkQuestionConfigurationTE"},{"$ref":"#/components/schemas/UpkQuestionConfigurationSLIDER"}],"discriminator":{"propertyName":"type","mapping":{"MC":"#/components/schemas/UpkQuestionConfigurationMC","SLIDER":"#/components/schemas/UpkQuestionConfigurationSLIDER","TE":"#/components/schemas/UpkQuestionConfigurationTE"}}},{"type":"null"}],"title":"Configuration"},"validation":{"anyOf":[{"$ref":"#/components/schemas/UpkQuestionValidation"},{"type":"null"}]},"importance":{"anyOf":[{"$ref":"#/components/schemas/UPKImportance"},{"type":"null"}]},"categories":{"items":{"$ref":"#/components/schemas/Category"},"type":"array","title":"Categories","description":"Categories associated with this question"},"explanation_template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation Template","description":"Human-readable template for explaining how a user's answer to this question affects eligibility","examples":["The company that administers your employer's retirement plan is {answer}."]},"explanation_fragment_template":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation Fragment Template","description":"A very short, natural-language explanation fragment that can be combined with others into a single sentence","examples":["whose employer's retirement plan is {answer}"]},"task_count":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Task Count","description":"The number of live Tasks that use this UPK Question","examples":[47]},"task_score":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Task Score","description":"GRL's internal ranked score for the UPK Question","examples":[0.11175522477414712]},"marketplace_task_count":{"anyOf":[{"additionalProperties":{"type":"integer","minimum":0.0},"propertyNames":{"$ref":"#/components/schemas/Source"},"type":"object"},{"type":"null"}],"title":"Marketplace Task Count","description":"The number of live Tasks that use this UPK Question per marketplace","examples":[{"d":23,"s":24}]}},"type":"object","required":["question_type","country_iso","language_iso","question_text","selector"],"title":"UpkQuestionOut","example":{"categories":[{"adwords_vertical_id":"396","label":"Politics","parent_uuid":"f66dddba61424ce5be2a38731450a0e1","path":"/News/Politics","uuid":"87b6d819f3ca4815bf1f135b1e829cc6"}],"choices":[{"choice_id":"1","choice_text":"Yes","exclusive":false,"order":0},{"choice_id":"2","choice_text":"No","exclusive":false,"order":1}],"country_iso":"us","importance":{"marketplace_task_count":{"d":9,"s":20,"w":20},"task_count":49,"task_score":3.3401743283265684},"language_iso":"eng","marketplace_task_count":{"d":9,"s":20,"w":20},"question_id":"fb20fd4773304500b39c4f6de0012a5a","question_text":"Are you registered to vote at your present address, or not?","question_type":"MC","selector":"SA","task_count":49,"task_score":3.3401743283265684}},"UpkQuestionResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"questions":{"items":{"$ref":"#/components/schemas/UpkQuestionOut"},"type":"array","title":"Questions"},"consent_questions":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Consent Questions","description":"For internal use"},"special_questions":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Special Questions","description":"For internal use"},"count":{"type":"integer","minimum":0.0,"title":"Count","description":"The number of questions returned"}},"type":"object","required":["status","questions","count"],"title":"UpkQuestionResponse"},"UpkQuestionSelectorHIDDEN":{"type":"string","enum":["HIDDEN"],"title":"UpkQuestionSelectorHIDDEN"},"UpkQuestionSelectorMC":{"type":"string","enum":["SA","MA","DL","SB","MSB"],"title":"UpkQuestionSelectorMC"},"UpkQuestionSelectorSLIDER":{"type":"string","enum":["HSLIDER","VSLIDER"],"title":"UpkQuestionSelectorSLIDER"},"UpkQuestionSelectorTE":{"type":"string","enum":["SL","ML","ETB"],"title":"UpkQuestionSelectorTE"},"UpkQuestionType":{"type":"string","enum":["MC","TE","SLIDER","HIDDEN"],"title":"UpkQuestionType"},"UpkQuestionValidation":{"properties":{"patterns":{"anyOf":[{"items":{"$ref":"#/components/schemas/PatternValidation"},"type":"array","minItems":1},{"type":"null"}],"title":"Patterns"}},"type":"object","required":["patterns"],"title":"UpkQuestionValidation"},"User":{"properties":{"id":{"anyOf":[{"type":"integer","exclusiveMaximum":2147483648.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Id"},"uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Uuid","examples":["0f85f1ec17f14621a06c687de5ddafca"]},"product":{"anyOf":[{"$ref":"#/components/schemas/Product"},{"type":"null"}]},"product_id":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Product Id","examples":["4fe381fb7186416cb443a38fa66c6557"]},"product_user_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":3},{"type":"null"}],"title":"Product User Id","description":"A unique identifier for each user, which is set by the Supplier. It should not contain any sensitive informationlike email or names, and should avoid using anyincrementing values.","examples":["app-user-9329ebd"]},"blocked":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Blocked","default":false},"created":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created","description":"When the user was created on the GRL platform."},"last_seen":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Seen","description":"When the user was last seen on, or acting on anypart of the GRL platform."},"audit_log":{"anyOf":[{"items":{"$ref":"#/components/schemas/AuditLog"},"type":"array"},{"type":"null"}],"title":"Audit Log"},"transactions":{"anyOf":[{"items":{"$ref":"#/components/schemas/LedgerTransaction"},"type":"array"},{"type":"null"}],"title":"Transactions"},"location_history":{"anyOf":[{"items":{"$ref":"#/components/schemas/GeoIPInformation"},"type":"array"},{"type":"null"}],"title":"Location History"}},"additionalProperties":false,"type":"object","title":"User"},"UserCreateConfig":{"properties":{"min_hourly_create_limit":{"type":"integer","minimum":0.0,"title":"Min Hourly Create Limit","description":"The smallest allowed value for the hourly user create limit.","default":0},"max_hourly_create_limit":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Max Hourly Create Limit","description":"The largest allowed value for the hourly user create limit. If None, the hourly create limit is unconstrained."}},"type":"object","title":"UserCreateConfig","description":"Stores configuration for the user creation experience.\n\nThe user creation limit is determined dynamically based on the median\n daily completion rate. min_hourly_create_limit &\n max_hourly_create_limit can be used to constrain the dynamically\n determined rate limit within set values."},"UserDisplayedWalletBalance":{"properties":{"currency":{"type":"string","maxLength":32,"title":"Currency"},"amount":{"type":"integer","title":"Amount","description":"Displayed balance in the currency's smallest ledger unit."},"amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amount String"}},"type":"object","required":["currency","amount"],"title":"UserDisplayedWalletBalance","description":"Combined user-visible balance for one ledger currency."},"UserHealthConfig":{"properties":{"banned_countries":{"items":{"type":"string","maxLength":2,"minLength":2,"pattern":"^[a-z]{2}$"},"type":"array","title":"Banned Countries"},"allow_ban_iphist":{"type":"boolean","title":"Allow Ban Iphist","default":true}},"type":"object","title":"UserHealthConfig"},"UserInfo":{"properties":{"user_profile_knowledge":{"items":{"$ref":"#/components/schemas/UserProfileKnowledge"},"type":"array","title":"User Profile Knowledge"},"marketplace_profile_knowledge":{"items":{"$ref":"#/components/schemas/MarketProfileKnowledge"},"type":"array","title":"Marketplace Profile Knowledge"}},"type":"object","title":"UserInfo"},"UserInfoResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"user_profile":{"$ref":"#/components/schemas/UserInfo"}},"type":"object","required":["status","user_profile"],"title":"UserInfoResponse"},"UserLedgerTransactionTaskAdjustment":{"properties":{"created":{"type":"string","format":"date-time","title":"Created","description":"When the Transaction was created"},"description":{"type":"string","maxLength":255,"title":"Description","description":"External description suitable for UI","default":"Task Adjustment"},"amount":{"type":"integer","title":"Amount","description":"The net amount affecting the user's wallet, in USDCents. Positive means the user's balance increased; negative means it decreased.","examples":[500,-250]},"balance_after":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Balance After"},"tx_type":{"type":"string","const":"bp_adjustment","title":"Tx Type","default":"bp_adjustment"},"tsid":{"type":"string","maxLength":32,"minLength":32,"title":"Tsid","description":"A unique identifier for the session","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"A link to where the user can get more details about this transaction","readOnly":true},"amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amount String","description":"The 'amount' with the payout_format applied.","readOnly":true}},"additionalProperties":false,"type":"object","required":["created","amount","tsid","url","amount_string"],"title":"UserLedgerTransactionTaskAdjustment","example":{"amount":-38,"amount_string":"$-0.38","created":"2026-09-09T18:52:57.514390Z","description":"Task Adjustment","tsid":"f9830cbc0fe345c394726265f15f9638","tx_type":"bp_adjustment","url":"https://fsb.generalresearch.com/4c1a9fb5bc8d46a8a5a07900662483bc/status/f9830cbc0fe345c394726265f15f9638/"}},"UserLedgerTransactionTaskComplete":{"properties":{"created":{"type":"string","format":"date-time","title":"Created","description":"When the Transaction was created"},"description":{"type":"string","maxLength":255,"title":"Description","description":"External description suitable for UI","default":"Task Complete"},"amount":{"type":"integer","title":"Amount","description":"The net amount affecting the user's wallet, in USDCents. Positive means the user's balance increased; negative means it decreased.","examples":[500,-250]},"balance_after":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Balance After"},"tx_type":{"type":"string","const":"bp_payment","title":"Tx Type","default":"bp_payment"},"tsid":{"type":"string","maxLength":32,"minLength":32,"title":"Tsid","description":"A unique identifier for the session","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"A link to where the user can get more details about this transaction","readOnly":true},"amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amount String","description":"The 'amount' with the payout_format applied.","readOnly":true}},"additionalProperties":false,"type":"object","required":["created","amount","tsid","url","amount_string"],"title":"UserLedgerTransactionTaskComplete","description":"In a BP with user wallet enabled, the task-complete transaction would have\nline items for both the credit to the bp_wallet_account and credit to\nuser_account. This is the user-detail, so we've only caring about the\nuser's payment.","example":{"amount":38,"amount_string":"$0.38","created":"2026-09-09T18:52:57.514841Z","description":"Task Complete","tsid":"423f5ae084304bbbbc1f6ffda1b1c21b","tx_type":"bp_payment","url":"https://fsb.generalresearch.com/1d00ab8d467646f39d142b4b7145db17/status/423f5ae084304bbbbc1f6ffda1b1c21b/"}},"UserLedgerTransactionTypeSummary":{"properties":{"entry_count":{"type":"integer","minimum":0.0,"title":"Entry Count","default":0},"min_amount":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Amount","description":"positive or negative USDCent"},"max_amount":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Amount","description":"positive or negative USDCent"},"total_amount":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Amount","description":"positive or negative USDCent"}},"type":"object","title":"UserLedgerTransactionTypeSummary"},"UserLedgerTransactionTypesSummary":{"properties":{"bp_adjustment":{"$ref":"#/components/schemas/UserLedgerTransactionTypeSummary"},"bp_payment":{"$ref":"#/components/schemas/UserLedgerTransactionTypeSummary"},"user_bonus":{"$ref":"#/components/schemas/UserLedgerTransactionTypeSummary"},"user_payout_request":{"$ref":"#/components/schemas/UserLedgerTransactionTypeSummary"}},"type":"object","title":"UserLedgerTransactionTypesSummary"},"UserLedgerTransactionUserBonus":{"properties":{"created":{"type":"string","format":"date-time","title":"Created","description":"When the Transaction was created"},"description":{"type":"string","maxLength":255,"title":"Description","description":"External description suitable for UI","default":"Compensation Bonus"},"amount":{"type":"integer","title":"Amount","description":"The net amount affecting the user's wallet, in USDCents. Positive means the user's balance increased; negative means it decreased.","examples":[500,-250]},"balance_after":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Balance After"},"tx_type":{"type":"string","const":"user_bonus","title":"Tx Type","default":"user_bonus"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"A link to where the user can get more details about this transaction","readOnly":true},"amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amount String","description":"The 'amount' with the payout_format applied.","readOnly":true}},"additionalProperties":false,"type":"object","required":["created","amount","url","amount_string"],"title":"UserLedgerTransactionUserBonus","example":{"amount":100,"amount_string":"$1.00","created":"2026-09-09T18:52:57.513921Z","description":"Compensation Bonus","tx_type":"user_bonus"}},"UserLedgerTransactionUserPayout":{"properties":{"created":{"type":"string","format":"date-time","title":"Created","description":"When the Transaction was created"},"description":{"type":"string","maxLength":255,"title":"Description","description":"External description suitable for UI"},"amount":{"type":"integer","title":"Amount","description":"The net amount affecting the user's wallet, in USDCents. Positive means the user's balance increased; negative means it decreased.","examples":[500,-250]},"balance_after":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Balance After"},"tx_type":{"type":"string","const":"user_payout_request","title":"Tx Type","default":"user_payout_request"},"payout_id":{"type":"string","maxLength":32,"minLength":32,"title":"Payout Id","description":"A unique identifier for the payout","examples":["a3848e0a53d64f68a74ced5f61b6eb68"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url","description":"A link to where the user can get more details about this transaction","readOnly":true},"amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amount String","description":"The 'amount' with the payout_format applied.","readOnly":true}},"additionalProperties":false,"type":"object","required":["created","description","amount","payout_id","url","amount_string"],"title":"UserLedgerTransactionUserPayout","example":{"amount":-5,"amount_string":"$-0.05","created":"2026-09-09T18:52:57.513399Z","description":"HIT Reward","payout_id":"af49e7a108c54c5b926ff97c0e177369","tx_type":"user_payout_request","url":"https://fsb.generalresearch.com/5ad58af0af3f49f39b8f480dfac856f3/cashout/af49e7a108c54c5b926ff97c0e177369/"}},"UserLedgerTransactionsResponse":{"properties":{"page":{"type":"integer","minimum":1.0,"title":"Page","description":"Page number","default":1},"size":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Size","description":"Page size","default":50},"total":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Total","description":"Total number of results"},"transactions":{"items":{"oneOf":[{"$ref":"#/components/schemas/UserLedgerTransactionUserPayout"},{"$ref":"#/components/schemas/UserLedgerTransactionUserBonus"},{"$ref":"#/components/schemas/UserLedgerTransactionTaskAdjustment"},{"$ref":"#/components/schemas/UserLedgerTransactionTaskComplete"}],"discriminator":{"propertyName":"tx_type","mapping":{"bp_adjustment":"#/components/schemas/UserLedgerTransactionTaskAdjustment","bp_payment":"#/components/schemas/UserLedgerTransactionTaskComplete","user_bonus":"#/components/schemas/UserLedgerTransactionUserBonus","user_payout_request":"#/components/schemas/UserLedgerTransactionUserPayout"}}},"type":"array","title":"Transactions"},"summary":{"$ref":"#/components/schemas/UserLedgerTransactionTypesSummary"},"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"pages":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pages","description":"Total number of pages","readOnly":true}},"type":"object","required":["summary","status","pages"],"title":"UserLedgerTransactionsResponse"},"UserLedgerWallet":{"properties":{"amount":{"type":"integer","title":"Amount","description":"(USD cents) The amount in the user's wallet."},"redeemable_amount":{"type":"integer","minimum":0.0,"title":"Redeemable Amount","description":"(USD cents) The amount in the user's wallet this is currently redeemable."},"payout_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payout Format"},"amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amount String"},"redeemable_amount_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Redeemable Amount String"},"account_uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Account Uuid","description":"A unique identifier for this Ledger Account","examples":["c3c3566b5b1b4961b63a5670a2dc923d"]},"account_type":{"type":"string","enum":["user_wallet","user_attempt_credit"],"title":"Account Type"},"currency":{"type":"string","maxLength":32,"title":"Currency","default":"USD"},"display_name":{"type":"string","maxLength":64,"title":"Display Name","description":"Human-readable description of the Ledger Account"}},"type":"object","required":["amount","redeemable_amount","account_uuid","account_type","display_name"],"title":"UserLedgerWallet","description":"A user-owned ledger account exposed by the wallets endpoint.","example":{"amount":123,"amount_string":"1230 Points","payout_format":"{payout*10:,.0f} Points","redeemable_amount":100,"redeemable_amount_string":"1000 Points"}},"UserLedgerWallets":{"properties":{"wallets":{"items":{"$ref":"#/components/schemas/UserLedgerWallet"},"type":"array","title":"Wallets"},"displayed_balances":{"items":{"$ref":"#/components/schemas/UserDisplayedWalletBalance"},"type":"array","title":"Displayed Balances"}},"type":"object","title":"UserLedgerWallets"},"UserPayoutEvent":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Payout Event Unique Identifier","examples":["9453cd076713426cb68d05591c7145aa"]},"debit_account_uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Debit Account Uuid","description":"The LedgerAccount.uuid that money is being requested from. Thie User or Brokerage Product is retrievable through the LedgerAccount.reference_uuid","examples":["18298cb1583846fbb06e4747b5310693"]},"cashout_method_uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Cashout Method Uuid","description":"References a row in the account_cashoutmethod table. This is the cashout method that was used to request this payout. (A cashout is the same thing as a payout)","examples":["a6dc1fc1bf934557b952f253dee12813"]},"created":{"type":"string","format":"date-time","title":"Created"},"amount":{"type":"integer","exclusiveMaximum":9.223372036854776e+18,"exclusiveMinimum":0.0,"title":"Amount","description":"The USDCent amount int. This cannot be 0 or negative","examples":[531]},"status":{"anyOf":[{"$ref":"#/components/schemas/PayoutStatus"},{"type":"null"}],"description":" - `PENDING` = PENDING\n - `APPROVED` = APPROVED\n - `REJECTED` = REJECTED\n - `CANCELLED` = CANCELLED\n - `FAILED` = FAILED\n - `COMPLETE` = COMPLETE","default":"PENDING","examples":["COMPLETE"]},"ext_ref_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ext Ref Id"},"payout_type":{"$ref":"#/components/schemas/PayoutType","description":" - `PAYPAL` = PAYPAL\n - `TANGO` = TANGO\n - `DWOLLA` = DWOLLA\n - `ACH` = ACH\n - `WIRE` = WIRE\n - `CASH_IN_MAIL` = CASH_IN_MAIL\n - `PRIZE` = PRIZE\n - `AMT` = AMT\n - `AMT_BONUS` = AMT_BONUS\n - `AMT_ASSIGNMENT` = AMT_HIT","examples":["ACH"]},"request_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Request Data","description":"Stores payout-type-specific information that is used to request this payout from the external provider."},"order_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"$ref":"#/components/schemas/CashMailOrderData"},{"type":"null"}],"title":"Order Data","description":"Stores payout-type-specific order information that is returned from the external payout provider."},"account_reference_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Account Reference Type"},"account_reference_uuid":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Account Reference Uuid"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","required":["debit_account_uuid","cashout_method_uuid","amount","payout_type"],"title":"UserPayoutEvent","description":"A user has requested to be paid from their wallet balance."},"UserProfile":{"properties":{"email_address":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email Address","examples":["contact@mail.com"]},"display_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Display Name","description":"A public name chosen by the user. Can be used in leaderboards or event stream."},"user":{"$ref":"#/components/schemas/User"},"marketplace_pids":{"additionalProperties":{"type":"string","maxLength":32,"minLength":32},"propertyNames":{"$ref":"#/components/schemas/Source"},"type":"object","title":"Marketplace Pids","description":"User's PID in marketplaces","examples":[{"c":"b507a2c00c3e481fb82f23655d142198","d":"deffe922063e4b9980206a62c3df2fba","i":"1dd9bd986794444eb97cb921aee5663f"}]},"streaks":{"items":{"$ref":"#/components/schemas/UserStreak"},"type":"array","title":"Streaks"},"email_md5":{"anyOf":[{"type":"string","maxLength":32,"minLength":32},{"type":"null"}],"title":"Email Md5","description":"MD5 hash of the email address","readOnly":true,"examples":["053fc3d5575362159e0c782abec83ffa"]},"email_sha1":{"anyOf":[{"type":"string","maxLength":40,"minLength":40},{"type":"null"}],"title":"Email Sha1","description":"SHA1 hash of the email address","readOnly":true,"examples":["6280fb76135b3585c0c5403be04844a0f0bae726"]},"email_sha256":{"anyOf":[{"type":"string","maxLength":64,"minLength":64},{"type":"null"}],"title":"Email Sha256","description":"SHA256 hash of the email address","readOnly":true,"examples":["8a098233e750f08de87d6053c06a58724287f34372368b6dc28b7ad4a77f3d39"]}},"additionalProperties":false,"type":"object","required":["user","email_md5","email_sha1","email_sha256"],"title":"UserProfile"},"UserProfileKnowledge":{"properties":{"property_id":{"type":"string","title":"Property Id"},"property_label":{"type":"string","title":"Property Label"},"translation":{"type":"string","title":"Translation"},"answer":{"items":{"$ref":"#/components/schemas/UserProfileKnowledgeAnswer"},"type":"array","title":"Answer"},"created":{"type":"string","format":"date-time","title":"Created","description":"When the User submitted this Profiling data"}},"type":"object","required":["property_id","property_label","translation","created"],"title":"UserProfileKnowledge"},"UserProfileKnowledgeAnswer":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"translation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Translation"},"value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Value"}},"type":"object","title":"UserProfileKnowledgeAnswer"},"UserProfileResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"user_profile":{"$ref":"#/components/schemas/UserProfile"}},"type":"object","required":["status","user_profile"],"title":"UserProfileResponse"},"UserProfileUpdateRequest":{"properties":{"email_address":{"type":"string","format":"email","title":"Email Address","examples":["contact@mail.com"]}},"additionalProperties":false,"type":"object","required":["email_address"],"title":"UserProfileUpdateRequest"},"UserProfilesResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"user_profiles":{"items":{"$ref":"#/components/schemas/UserProfile"},"type":"array","title":"User Profiles"}},"type":"object","required":["status","user_profiles"],"title":"UserProfilesResponse"},"UserQuestionAnswerIn":{"properties":{"question_id":{"type":"string","maxLength":32,"minLength":32,"title":"Question Id","examples":["fb20fd4773304500b39c4f6de0012a5a"]},"answer":{"items":{"type":"string","maxLength":5000,"minLength":1},"type":"array","maxItems":10,"minItems":1,"title":"Answer","description":"The user's answers to this question. Must pass the choice_id if the question is a Multiple Choice, or the actual text if the question is Text Entry","examples":[["1"]]}},"additionalProperties":false,"type":"object","required":["question_id","answer"],"title":"UserQuestionAnswerIn","description":"Send the answers to one or more questions for a user. A question is\nuniquely specified by the question_id key. The answer is: the choice_id\nif the question_type is \"MC\" the actual entered text if the\nquestion_type is \"TE\"\n\nTODO: look up the question_type from the question_id to apply MC or\n TE specific validation on the answer(s)"},"UserStreak":{"properties":{"country_iso":{"type":"string","title":"Country Iso"},"period":{"$ref":"#/components/schemas/StreakPeriod"},"fulfillment":{"$ref":"#/components/schemas/StreakFulfillment"},"current_streak":{"type":"integer","minimum":0.0,"title":"Current Streak"},"longest_streak":{"type":"integer","minimum":0.0,"title":"Longest Streak"},"state":{"$ref":"#/components/schemas/StreakState"},"last_fulfilled_period_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Last Fulfilled Period Start"},"timezone_name":{"type":"string","title":"Timezone Name","readOnly":true},"current_period_bounds":{"prefixItems":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}],"type":"array","maxItems":2,"minItems":2,"title":"Current Period Bounds","readOnly":true},"last_fulfilled_period_bounds":{"anyOf":[{"prefixItems":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}],"type":"array","maxItems":2,"minItems":2},{"type":"null"}],"title":"Last Fulfilled Period Bounds","readOnly":true},"time_remaining_in_period":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Time Remaining In Period","readOnly":true}},"additionalProperties":false,"type":"object","required":["country_iso","period","fulfillment","current_streak","longest_streak","state","timezone_name","current_period_bounds","last_fulfilled_period_bounds","time_remaining_in_period"],"title":"UserStreak"},"UserWalletBalance":{"properties":{"amount":{"type":"integer","title":"Amount","description":"(USD cents) The amount in the user's wallet."},"redeemable_amount":{"type":"integer","minimum":0.0,"title":"Redeemable Amount","description":"(USD cents) The amount in the user's wallet this is currently redeemable."},"payout_format":{"type":"string","title":"Payout Format","description":"\nThe format describing the str representation of a payout. Typically, this would be displayed to a user.\nThe payout_format is similar to python format string with a subset of functionality supported.\nOnly float with a precision are supported along with an optional comma for a thousands separator.\nIn addition, a mathematical operator can be applied, such as dividing by 100. \nExamples are shown assuming payout = 100 (one dollar).\n- \"{payout*10:,.0f} Points\" -> \"1,000 Points\"\n- \"${payout/100:.2f}\" -> \"$1.00\"\n","examples":["{payout*10:,.0f} Points","${payout/100:.2f}","{payout:.0f}"]},"amount_string":{"type":"string","title":"Amount String","description":"The 'amount' with the payout_format applied. Can be displayed to the user."},"redeemable_amount_string":{"type":"string","title":"Redeemable Amount String","description":"The 'redeemable_amount' with the payout_format applied. Can be displayed to the user."}},"type":"object","required":["amount","redeemable_amount","payout_format","amount_string","redeemable_amount_string"],"title":"UserWalletBalance","example":{"amount":123,"amount_string":"1230 Points","payout_format":"{payout*10:,.0f} Points","redeemable_amount":100,"redeemable_amount_string":"1000 Points"}},"UserWalletBalanceResponse":{"properties":{"status":{"type":"string","enum":["success","error"],"title":"Status","description":"The status of the API response.","examples":["success"]},"msg":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Msg","description":"An optional message, if the status is error.","examples":[""]},"wallet":{"$ref":"#/components/schemas/UserWalletBalance"}},"type":"object","required":["status","wallet"],"title":"UserWalletBalanceResponse"},"UserWalletConfig":{"properties":{"enabled":{"type":"boolean","title":"Enabled","description":"If enabled, the users' wallets are managed.","default":false},"balance_type":{"type":"string","enum":["wallet_balance","redeemable_balance"],"title":"Balance Type","description":"The balance used as the user's displayed balance.","default":"wallet_balance"},"amt":{"type":"boolean","title":"Amt","description":"Uses Amazon Mechanical Turk","default":false},"supported_payout_types":{"items":{"$ref":"#/components/schemas/PayoutType"},"type":"array","title":"Supported Payout Types"},"min_cashout":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Min Cashout","description":"Minimum cashout amount. If enabled is True and no min_cashout is set, will default to $0.01.","examples":["10.00"]},"failed_attempt_credit":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Failed Attempt Credit","description":"Conditional credit awarded for an eligible failed attempt. None disables failed-attempt credits.","examples":["0.05",null]}},"type":"object","title":"UserWalletConfig","description":"Stores configuration for the user wallet handling"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WXETOfferwall":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier to reference a generated offerwall","examples":["7dc1d3aeb4844a6fab17ecd370b8bf1e"]},"availability_count":{"type":"integer","minimum":0.0,"title":"Availability Count","description":"Total opportunities available for specific bpuid respondent and parameters. This value changes frequently and can be used to determine if a respondent has potential tasks available, regardless of the offerwall type being requested. If the value is 0, no buckets will be generated.","examples":[42]},"attempted_live_eligible_count":{"type":"integer","minimum":0.0,"title":"Attempted Live Eligible Count","description":"Number of currently live opportunities for which the respondent meets all eligibility criteria but is excluded due to a prior attempt. Only includes surveys that are still live and otherwise eligible; does not include previously attempted surveys that are no longer available.","default":0,"examples":[7]},"buckets":{"items":{"$ref":"#/components/schemas/WXETOfferwallBucket"},"type":"array","title":"Buckets"},"offerwall_reasons":{"items":{"$ref":"#/components/schemas/OfferwallReason"},"type":"array","title":"Offerwall Reasons","description":"Explanations describing why so many or few opportunities are available.","examples":[["USER_BLOCKED","UNDER_MINIMUM_AGE"]]}},"additionalProperties":false,"type":"object","required":["id","availability_count"],"title":"WXETOfferwall","description":"Returns buckets from WXET as single tasks\nOfferwall code: `55a4e1a9`"},"WXETOfferwallBucket":{"properties":{"id":{"type":"string","maxLength":32,"minLength":32,"title":"Id","description":"Unique identifier this particular bucket","examples":["5ba2fe5010cc4d078fc3cc0b0cc264c3"]},"uri":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Uri","description":"The URL to send a respondent into. Must not edit this URL in any way","examples":["https://task.generalresearch.com/api/v1/52d3f63b2709/797df4136c604a6c8599818296aae6d1/?i=5ba2fe5010cc4d078fc3cc0b0cc264c3&b=test&66482fb=e7baf5e"]},"duration":{"type":"integer","maximum":5400.0,"exclusiveMinimum":0.0,"title":"Duration","description":"The bucket's expected duration, in seconds"},"min_payout":{"type":"integer","maximum":10000.0,"exclusiveMinimum":0.0,"title":"Min Payout","description":"The bucket's min payout, in usd cents"}},"additionalProperties":false,"type":"object","required":["id","uri","duration","min_payout"],"title":"WXETOfferwallBucket"},"WXETOfferwallResponse":{"properties":{"info":{"$ref":"#/components/schemas/OfferWallInfo"},"offerwall":{"$ref":"#/components/schemas/WXETOfferwall"}},"type":"object","required":["info","offerwall"],"title":"WXETOfferwallResponse"},"WallAdjustedStatus":{"type":"string","enum":["ac","af","ca","cc"],"title":"WallAdjustedStatus"},"WallOut":{"properties":{"uuid":{"type":"string","maxLength":32,"minLength":32,"title":"Uuid"},"source":{"$ref":"#/components/schemas/Source"},"buyer_id":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Buyer Id"},"req_survey_id":{"type":"string","maxLength":32,"title":"Req Survey Id"},"started":{"type":"string","format":"date-time","title":"Started"},"survey_id":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Survey Id"},"finished":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Finished"},"status":{"anyOf":[{"$ref":"#/components/schemas/Status"},{"type":"null"}]},"status_code_1":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"__High level status code for outcome of the session.__\nThis should only be NULL if the Status is ABANDON or TIMEOUT\n\nAllowed values: \n - __BUYER_FAIL__: User terminated in buyer survey\n - __BUYER_QUALITY_FAIL__: User terminated in buyer survey for quality reasons\n - __PS_FAIL__: User failed in marketplace's prescreener\n - __PS_QUALITY__: User rejected by marketplace for quality reasons\n - __PS_BLOCKED__: User is explicitly blocked by the marketplace. Note: on some marketplaces, users can have multiple PS_QUALITY terminations and still complete surveys.\n - __PS_OVERQUOTA__: User rejected by marketplace for over quota\n - __PS_DUPLICATE__: User rejected by marketplace for duplicate\n - __GRS_FAIL__: The user failed within the GRS Platform\n - __GRS_QUALITY_FAIL__: The user failed within the GRS Platform for quality reasons\n - __GRS_ABANDON__: The user abandoned/timed out within the GRS Platform\n - __PS_ABANDON__: The user abandoned/timed out within the marketplace's pre-screen system. Note: On most marketplaces, we have no way of distinguishing between this and BUYER_ABANDON. BUYER_ABANDON is used as the default, unless we know it is PS_ABANDON.\n - __BUYER_ABANDON__: The user abandoned/timed out within the client survey\n - __UNKNOWN__: The status code is not documented\n - __COMPLETE__: The user completed the task successfully\n - __MARKETPLACE_FAIL__: Something was wrong upon the user redirecting from the marketplace, e.g. no postback received, or url hashing failures.\n - __SESSION_START_FAIL__: User failed before being sent into a marketplace\n - __SESSION_CONTINUE_FAIL__: User failed between attempts\n - __SESSION_START_QUALITY_FAIL__: User failed before being sent into a marketplace for \"security\" reasons\n - __SESSION_CONTINUE_QUALITY_FAIL__: User failed between attempts for \"security\" reasons","examples":["COMPLETE"]},"status_code_2":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"This should be set if the Wall.status_code_1 is MARKETPLACE_FAIL\n\nAllowed values: \n - __URL_HASHING_CHECK_FAILED__: The redirect URL (coming back from the marketplace) failed hashing checks\n - __BROKEN_REDIRECT__: The redirect URL was missing required query params or was unparseable\n - __INVALID_REDIRECT__: The redirect URL was invalid or inconsistent in some way and as a result we could not determine the outcome. This could be if a redirect received did not match the user's most recent attempt.\n - __INVALID_MARKETPLACE_POSTBACK__: The redirect indicated a complete, but no/invalid Postback was received from the marketplace\n - __NO_MARKETPLACE_POSTBACK__: No/invalid Postback was received from the marketplace. Used in cases where the redirect does not contain a status.\n - __COMPLETE_TOO_FAST__: The marketplace indicates the user completed the survey, but we don't think this is valid due to speeding. Generally this cutoff is the 95th percentile of our calculated CompletionTime survey stat.\n - __INTERNAL_ERROR__: Something happened during the handling of this redirect (on our side)","examples":[null]},"ext_status_code_1":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Ext Status Code 1"},"ext_status_code_2":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Ext Status Code 2"},"ext_status_code_3":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Ext Status Code 3"},"report_value":{"anyOf":[{"$ref":"#/components/schemas/ReportValue"},{"type":"null"}]},"report_notes":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Report Notes"},"adjusted_status":{"anyOf":[{"$ref":"#/components/schemas/WallAdjustedStatus"},{"type":"null"}]},"adjusted_timestamp":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Adjusted Timestamp"},"user_cpi":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Cpi","description":"\n The amount the user would earn from completing this task, if the status was a complete.\n If the BP has no payout xform, the user_cpi is None. This is analogous to the session's\n user_payout.\n ","examples":[123]},"user_cpi_string":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Cpi String","description":"If a payout transformation is configured on this account, this is the amount to display to the user","examples":["123 Points"]}},"additionalProperties":false,"type":"object","required":["source","req_survey_id"],"title":"WallOut"},"WallStatusCode2":{"type":"integer","enum":[12,16,17,13,18,14,15],"title":"WallStatusCode2","description":"This should be set if the Wall.status_code_1 is MARKETPLACE_FAIL"},"YieldManConfig":{"properties":{"category_request":{"$ref":"#/components/schemas/OfferWallCategoryRequest"},"scoring_params":{"$ref":"#/components/schemas/OfferWallRequestYieldmanParams"}},"type":"object","title":"YieldManConfig"}}},"tags":[{"name":"Offerwall","description":"Generate and retrieve various offerwall groupings. These results should be called \"on the \n fly\" just before presenting the results to a respondent. Results greater than 3 minutes old should not be \n presented to the respondent. When a respondent takes any action on a generated offerwall (they go into a \n bucket), the offerwall should be discarded. Any subsequent bucket entrances will fail. The following reserved \n are reserved keywords. However, any additional query parameter that is provided, will be returned back in the \n TaskResponse response in the kwargs attribute.\n\n {'b', 'bp_user_id', 'bpuid', 'country_codes', 'country', 'cpi', 'currency', \n 'description', 'duration', 'entry_link', 'i', 'idx', 'info', 'ip', 'kwargs',\n 'l', 'lang_codes', 'languages', 'loi', 'name', 'payout', 'product_id', \n 'quality', 'quality_category', 'req_loi', 'status', 'task_uri', 'tasks', \n 'tsid', 'uri', 'url', 'country_iso', 'format', 'behavior'}"},{"name":"Status","description":"Retrieving the status of respondent attempts, either by brokerage account filtering, or a specific Task Status ID (TSID) that is provided in the redirect."}]}
\ No newline at end of file diff --git a/jb-ui/src/components/EventMarquee.tsx b/jb-ui/src/components/EventMarquee.tsx index 4ce4f15..46508ff 100644 --- a/jb-ui/src/components/EventMarquee.tsx +++ b/jb-ui/src/components/EventMarquee.tsx @@ -1,230 +1,250 @@ -import { EventEnvelope, EventType, TaskEnterPayload, TaskFinishPayload } from "@/api_fsb"; -import { formatSecondsVerbose, formatSource, formatStatus, truncate } from "@/lib/utils"; +import { + EventEnvelope, + EventType, + TaskEnterPayload, + TaskFinishPayload, +} from "@/api_fsb"; +import { + formatSecondsVerbose, + formatSource, + formatStatus, + truncate, +} from "@/lib/utils"; import { selectAllEvents, selectCurrentSpeed } from "@/models/grlEventsSlice"; -import getUnicodeFlagIcon from 'country-flag-icons/unicode'; +import getUnicodeFlagIcon from "country-flag-icons/unicode"; import { useEffect, useRef, useState } from "react"; import { useSelector } from "react-redux"; // ─── Config ─────────────────────────────────────────────────────────────────── -const SPEED_EASING = 0.025; // how quickly speed transitions (per frame) +const SPEED_EASING = 0.025; // how quickly speed transitions (per frame) interface EventItemProps { - event: EventEnvelope; + event: EventEnvelope; } const EventTaskEnter = ({ event }: EventItemProps) => { - const payload = event.payload as TaskEnterPayload; - - return ( - <> - <span className="px-2 text-lg">{getUnicodeFlagIcon(payload.country_iso)}</span> - <p> - {truncate(event.product_user_id!, 6, "*****")} Entered{" "} - {formatSource(payload.source)}{" "} - (#{truncate(payload.survey_id!, 5, "***")}) - </p> - </> - ) -} + const payload = event.payload as TaskEnterPayload; + + return ( + <> + <span className="px-2 text-lg"> + {getUnicodeFlagIcon(payload.country_iso)} + </span> + <p> + {truncate(event.product_user_id!, 6, "*****")} Entered{" "} + {formatSource(payload.source)} (# + {truncate(payload.survey_id!, 5, "***")}) + </p> + </> + ); +}; const EventTaskFinish = ({ event }: EventItemProps) => { - const payload = event.payload as TaskFinishPayload; - - return ( - <> - <span className="px-2 text-lg">{getUnicodeFlagIcon(event.payload.country_iso)}</span> - <p> - {truncate(event.product_user_id!, 6, "*****")}{" "} - {formatStatus(payload.status)} {formatSource(payload.source)}{" "} - (#{truncate(payload.survey_id!, 5, "***")}) in{" "} - {formatSecondsVerbose(payload.duration_sec!)} - </p> - </> - ) -} + const payload = event.payload as TaskFinishPayload; + + return ( + <> + <span className="px-2 text-lg"> + {getUnicodeFlagIcon(event.payload.country_iso)} + </span> + <p> + {truncate(event.product_user_id!, 6, "*****")}{" "} + {formatStatus(payload.status)} {formatSource(payload.source)} (# + {truncate(payload.survey_id!, 5, "***")}) in{" "} + {formatSecondsVerbose(payload.duration_sec!)} + </p> + </> + ); +}; const EventUserCreated = ({ event }: EventItemProps) => { - - return ( - <> - <span className="px-2 text-lg">{getUnicodeFlagIcon(event.payload.country_iso)}</span> - <p>{truncate(event.product_user_id!, 6, "*****")} Created</p> - </> - ) -} - + return ( + <> + <span className="px-2 text-lg"> + {getUnicodeFlagIcon(event.payload.country_iso)} + </span> + <p>{truncate(event.product_user_id!, 6, "*****")} Created</p> + </> + ); +}; const EventComponent = ({ event }: EventItemProps) => { - - const renderContent = () => { - switch (event.event_type) { - case EventType.TaskEnter: - return <EventTaskEnter event={event} /> - case EventType.TaskFinish: - return <EventTaskFinish event={event} /> - case EventType.UserCreated: - return <EventUserCreated event={event} /> - default: - return <><p>Unknown event</p></> - } + const renderContent = () => { + switch (event.event_type) { + case EventType.TaskEnter: + return <EventTaskEnter event={event} />; + case EventType.TaskFinish: + return <EventTaskFinish event={event} />; + case EventType.UserCreated: + return <EventUserCreated event={event} />; + default: + return ( + <> + <p>Unknown event</p> + </> + ); } + }; - return ( - <div key={event.event_uuid!} - className="inline-flex items-center text-[10px] - font-mono px-2"> - {renderContent()} - - {/* <span className="text-red-500 pl-5">◆</span> */} - </div > - ) -} + return ( + <div + key={event.event_uuid!} + className="inline-flex items-center text-[10px] + font-mono px-2" + > + {renderContent()} + {/* <span className="text-red-500 pl-5">◆</span> */} + </div> + ); +}; // ─── Main Component ─────────────────────────────────────────────────────────── export default function NewsTicker() { - // --- Redux --- - const reduxItems = useSelector(selectAllEvents); - const targetSpeedRedux = useSelector(selectCurrentSpeed); // px/sec - - // --- Scroll state (all refs — no re-renders from animation) --- - const containerRef = useRef<HTMLDivElement | null>(null); // The ticker band - const stripRef = useRef<HTMLDivElement | null>(null); // Scrolling strip - const translateX = useRef(0); - const currentSpeed = useRef(targetSpeedRedux); - const targetSpeed = useRef(targetSpeedRedux); - - // Only state and refs persist across renders, the rest are re-initialized, - // that is why this isn't just a standard variable. - // Request Animation Frame reference (stores the animation frame ID) - const rafRef = useRef<number | null>(null); - const lastTime = useRef<number | null>(null); - - // --- Item tracking --- - // Track which IDs are already in the scroller to only append - // genuinely new ones. - const seenUUIDs = useRef(new Set<string>()); - const pendingQueue = useRef<EventEnvelope[]>([]); - const [scrollItems, setScrollItems] = useState<EventEnvelope[]>([]); - - // --- Sync Redux → pendingQueue (only new items) --- - useEffect(() => { - const newItems = reduxItems.filter((item) => !seenUUIDs.current.has(item.event_uuid!)); - if (newItems.length === 0) return; - - newItems.forEach(item => seenUUIDs.current.add(item.event_uuid!)); - pendingQueue.current.push(...newItems); - }, [reduxItems]); - - // --- Sync target speed from Redux --- - useEffect(() => { - targetSpeed.current = targetSpeedRedux; - }, [targetSpeedRedux]); - - // --- RAF loop --- - useEffect(() => { - // This timestamp is a "high-resolution timestamp" provided by - // the browser, which is more accurate for measuring frame intervals - // than Date.now() - const loop = (timestamp: number) => { - console.log("RAF loop, timestamp:", timestamp, lastTime.current); - - if (!lastTime.current) lastTime.current = timestamp; - const delta = (timestamp - lastTime.current) / 1000; - lastTime.current = timestamp; - - // Slow easement towards targetSpeed - currentSpeed.current += - (targetSpeed.current - currentSpeed.current) * SPEED_EASING; - - // flush pending queue into React state - if (pendingQueue.current.length > 0) { - const incoming = pendingQueue.current.splice(0); - setScrollItems((prev) => [...prev, ...incoming]); - } - - // Advance strip, slides it over left by X pixels - translateX.current -= currentSpeed.current * delta; - if (stripRef.current) { - stripRef.current.style.transform = `translateX(${translateX.current}px)`; - } - - // prune items that have fully exited the left edge - if (stripRef.current && containerRef.current) { - const children = Array.from(stripRef.current.children); - const containerLeft = containerRef.current.getBoundingClientRect().left; - let pruneCount = 0; - let removedWidth = 0; - - for (const child of children) { - const rect = child.getBoundingClientRect(); - if (rect.right < containerLeft - 20) { - pruneCount++; - removedWidth += rect.width; - } else { - break; - } - } - - if (pruneCount > 0) { - translateX.current += removedWidth; - setScrollItems((prev) => prev.slice(pruneCount)); - } - } - - // Assign the next loop - rafRef.current = requestAnimationFrame(loop); - }; - - rafRef.current = requestAnimationFrame(loop); - return () => cancelAnimationFrame(rafRef.current!); - }, []); - - return ( - <div style={{ - minHeight: "3vh", display: "flex", flexDirection: "column", - justifyContent: "center", alignItems: "center", gap: "0" - }}> - - {/* Ticker wrapper */} - <div style={{ width: "100%", position: "relative" }}> - - {/* The ticker band */} - <div - ref={containerRef} - style={{ - overflow: "hidden", - padding: "0", - position: "relative", - }} - > - - {/* Scrolling strip */} - <div - ref={stripRef} - style={{ - display: "inline-flex", - alignItems: "center", - whiteSpace: "nowrap", - padding: "10px 0", - willChange: "transform", - position: "relative", - zIndex: 2, - }} - > - {scrollItems.map(item => ( - <EventComponent event={item} /> - ))} - - {/* Trailing spacer so new content doesn't immediately snap in */} - <span style={{ paddingRight: "100vw", display: "inline-block" }} /> - - </div> - </div> - - </div> + // --- Redux --- + const reduxItems = useSelector(selectAllEvents); + const targetSpeedRedux = useSelector(selectCurrentSpeed); // px/sec + + // --- Scroll state (all refs — no re-renders from animation) --- + const containerRef = useRef<HTMLDivElement | null>(null); // The ticker band + const stripRef = useRef<HTMLDivElement | null>(null); // Scrolling strip + const translateX = useRef(0); + const currentSpeed = useRef(targetSpeedRedux); + const targetSpeed = useRef(targetSpeedRedux); + + // Only state and refs persist across renders, the rest are re-initialized, + // that is why this isn't just a standard variable. + // Request Animation Frame reference (stores the animation frame ID) + const rafRef = useRef<number | null>(null); + const lastTime = useRef<number | null>(null); + + // --- Item tracking --- + // Track which IDs are already in the scroller to only append + // genuinely new ones. + const seenUUIDs = useRef(new Set<string>()); + const pendingQueue = useRef<EventEnvelope[]>([]); + const [scrollItems, setScrollItems] = useState<EventEnvelope[]>([]); + + // --- Sync Redux → pendingQueue (only new items) --- + useEffect(() => { + const newItems = reduxItems.filter( + (item) => !seenUUIDs.current.has(item.event_uuid!), + ); + if (newItems.length === 0) return; + + newItems.forEach((item) => seenUUIDs.current.add(item.event_uuid!)); + pendingQueue.current.push(...newItems); + }, [reduxItems]); + + // --- Sync target speed from Redux --- + useEffect(() => { + targetSpeed.current = targetSpeedRedux; + }, [targetSpeedRedux]); + + // --- RAF loop --- + useEffect(() => { + // This timestamp is a "high-resolution timestamp" provided by + // the browser, which is more accurate for measuring frame intervals + // than Date.now() + const loop = (timestamp: number) => { + // console.log("RAF loop, timestamp:", timestamp, lastTime.current); + + if (!lastTime.current) lastTime.current = timestamp; + const delta = (timestamp - lastTime.current) / 1000; + lastTime.current = timestamp; + + // Slow easement towards targetSpeed + currentSpeed.current += + (targetSpeed.current - currentSpeed.current) * SPEED_EASING; + + // flush pending queue into React state + if (pendingQueue.current.length > 0) { + const incoming = pendingQueue.current.splice(0); + setScrollItems((prev) => [...prev, ...incoming]); + } + + // Advance strip, slides it over left by X pixels + translateX.current -= currentSpeed.current * delta; + if (stripRef.current) { + stripRef.current.style.transform = `translateX(${translateX.current}px)`; + } + + // prune items that have fully exited the left edge + if (stripRef.current && containerRef.current) { + const children = Array.from(stripRef.current.children); + const containerLeft = containerRef.current.getBoundingClientRect().left; + let pruneCount = 0; + let removedWidth = 0; + + for (const child of children) { + const rect = child.getBoundingClientRect(); + if (rect.right < containerLeft - 20) { + pruneCount++; + removedWidth += rect.width; + } else { + break; + } + } + if (pruneCount > 0) { + translateX.current += removedWidth; + setScrollItems((prev) => prev.slice(pruneCount)); + } + } + + // Assign the next loop + rafRef.current = requestAnimationFrame(loop); + }; + + rafRef.current = requestAnimationFrame(loop); + return () => cancelAnimationFrame(rafRef.current!); + }, []); + + return ( + <div + style={{ + minHeight: "3vh", + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + gap: "0", + }} + > + {/* Ticker wrapper */} + <div style={{ width: "100%", position: "relative" }}> + {/* The ticker band */} + <div + ref={containerRef} + style={{ + overflow: "hidden", + padding: "0", + position: "relative", + }} + > + {/* Scrolling strip */} + <div + ref={stripRef} + style={{ + display: "inline-flex", + alignItems: "center", + whiteSpace: "nowrap", + padding: "10px 0", + willChange: "transform", + position: "relative", + zIndex: 2, + }} + > + {scrollItems.map((item) => ( + <EventComponent event={item} /> + ))} + + {/* Trailing spacer so new content doesn't immediately snap in */} + <span style={{ paddingRight: "100vw", display: "inline-block" }} /> + </div> </div> - ); + </div> + </div> + ); } diff --git a/jb-ui/src/components/Footer.tsx b/jb-ui/src/components/Footer.tsx index 00c0101..e65255f 100644 --- a/jb-ui/src/components/Footer.tsx +++ b/jb-ui/src/components/Footer.tsx @@ -1,41 +1,50 @@ -import { useAppSelector } from "@/hooks"; +import { useGetMeQuery } from "@/models/authAPI"; const Footer = () => { - const bpuid = useAppSelector(state => state.app.bpuid) - const assignment_id = useAppSelector(state => state.app.assignment_id) + const { data: user } = useGetMeQuery(); - return ( - <div className="grid gap-1 grid-cols-1 + return ( + <div + className="grid gap-1 grid-cols-1 md:grid-cols-3 - text-xs"> - <div> - <p>help: <a - href={`mailto:support@jamesbillings67.com?subject=AMT Support Request: (${(bpuid ?? '-')}) ${(assignment_id ?? '–')}`}> - support@jamesbillings67.com - </a> - </p> - </div> + text-xs" + > + <div> + <p> + help:{" "} + <a + href={`mailto:support@jamesbillings67.com?subject=AMT Support Request: (${user?.product_user_id ?? "-"})`} + > + support@jamesbillings67.com + </a> + </p> + </div> - <div> - <p>made with <span className="text-rose-500">♥</span> by <a - className="hover:cursor-pointer hover:text-pink" - href="https://instagram.com/x0xMaximus" - target="_blank">Max Nanis</a> - </p> - </div> + <div> + <p> + made with <span className="text-rose-500">♥</span> by{" "} + <a + className="hover:cursor-pointer hover:text-pink" + href="https://instagram.com/x0xMaximus" + target="_blank" + > + Max Nanis + </a> + </p> + </div> - <div> - <p> - <a href="https://git.generalresearch.com/panels/amt-jb/" - target="_blank"> - open source - </a> - </p> - </div> + <div> + <p> + <a + href="https://git.generalresearch.com/panels/amt-jb/" + target="_blank" + > + open source + </a> + </p> + </div> + </div> + ); +}; - </div> - ) - -} - -export default Footer
\ No newline at end of file +export default Footer; diff --git a/jb-ui/src/components/Profiling.tsx b/jb-ui/src/components/Profiling.tsx index 9cefdda..b834c9f 100644 --- a/jb-ui/src/components/Profiling.tsx +++ b/jb-ui/src/components/Profiling.tsx @@ -1,536 +1,572 @@ -import { - UpkQuestionChoice -} from "@/api_fsb"; -import { - ChevronRightIcon -} from "lucide-react"; +import { UpkQuestionChoice } from "@/api_fsb"; +import { useGetMeQuery } from "@/models/authAPI"; +import { ChevronRightIcon } from "lucide-react"; import { KeyboardEvent, MouseEvent, useEffect, useRef, useState } from "react"; -import { useAppDispatch, useAppSelector } from "@/hooks"; +import { useAppDispatch } from "@/hooks"; import { bpid } from "@/lib/utils"; import { - createColumnHelper, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getSortedRowModel, - RowModel, - RowSelectionState, - useReactTable, -} from '@tanstack/react-table'; + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + RowModel, + RowSelectionState, + useReactTable, +} from "@tanstack/react-table"; import { clsx } from "clsx"; import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, } from "@/components/ui/pagination"; import { - addAnswers, - fetchNewQuestions, - goToNextQuestion, - ProfileQuestion, - questionsSelectors, - saveAnswer, - selectActiveQuestion, - selectActiveQuestionValidation, - selectAllQuestions, - setActiveQuestion + addAnswers, + fetchNewQuestions, + goToNextQuestion, + ProfileQuestion, + questionsSelectors, + saveAnswer, + selectActiveQuestion, + selectActiveQuestionValidation, + selectAllQuestions, + setActiveQuestion, } from "@/models/profilingQuestionsSlice"; import { useSelector } from "react-redux"; // import {motion} from "framer-motion" const TextEntry = () => { - const dispatch = useAppDispatch() - - const activeQuestion = useSelector(selectActiveQuestion); - // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); - // const selectAnswer = useSelector(selectAnswerForQuestion(question)); - const metadata = activeQuestion?._metadata - const validation = activeQuestion?._validation - const bpuid = useAppSelector(state => state.app.bpuid) - - if (!metadata) return null - if (!validation) return null - - const error = validation?.errors.length > 0 - - const handleInputChange = (event: React.KeyboardEvent<HTMLInputElement>) => { - if (!activeQuestion.question_id) return; - - const target = event.target as HTMLInputElement; - dispatch(addAnswers({ questionId: activeQuestion.question_id!, answers: [target.value] as string[] })) - }; - - const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => { - if (!activeQuestion.question_id) return; - if (!bpuid) return; - - if (event.key === 'Enter') { - dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) - } - }; - - return ( - <div className="px-6 my-6"> - <input type="text" - id="text-entry-input" - aria-describedby="" - // defaultValue={answer?.values.length ? answer?.values[0] : ""} - onKeyUp={handleInputChange} - onKeyDown={handleKeyPress} // Use onKeyDown to listen for the key press - // title={error ? answer?.error_msg : ""} - className={clsx( - 'w-full border-1 rounded px-2 py-1', - error ? - 'border-red-500 focus-visible:ring-red-500' : - 'border-blue-50' - )} - /> - - { - validation.errors.map(e => { - return ( - <p className="text-sm text-red-100 my-2">{e.message}</p> - ) - }) - } - </div> - ) -} - - -export default function useKeyboardMode( - rowModel: RowModel<UpkQuestionChoice>, -) { - // const [keyboardEnabled, setKeyboardEnabled] = useState( - // localStorage.getItem(keyboardMode) === "enabled", - // ); - const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]); - - const dispatch = useAppDispatch() - const activeQuestion = useSelector(selectActiveQuestion); - const bpuid = useAppSelector(state => state.app.bpuid) - - const keyboardSelectionIdxRef = useRef(0); - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - - if (!activeQuestion) return; - if (!bpuid) return; - - // if (!keyboardEnabled) { - // return; - // } - - const { key } = event; - const totalRows = rowRefs.current.length; - const prevIdx = keyboardSelectionIdxRef.current; - let nextIdx = prevIdx; - - if (key === "ArrowDown") nextIdx = Math.min(prevIdx + 1, totalRows - 1); - if (key === "ArrowUp") nextIdx = Math.max(prevIdx - 1, 0); - if (key === " ") { - const active = document.activeElement as HTMLElement | null; - - if (active?.tabIndex === 0) { - return; - } else { - rowModel.rows[keyboardSelectionIdxRef.current].toggleSelected(); - } - } + const dispatch = useAppDispatch(); + + const activeQuestion = useSelector(selectActiveQuestion); + // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); + // const selectAnswer = useSelector(selectAnswerForQuestion(question)); + const metadata = activeQuestion?._metadata; + const validation = activeQuestion?._validation; + const { data: user } = useGetMeQuery(); + + if (!metadata) return null; + if (!validation) return null; + + const error = validation?.errors.length > 0; + + const handleInputChange = (event: React.KeyboardEvent<HTMLInputElement>) => { + if (!activeQuestion.question_id) return; + + const target = event.target as HTMLInputElement; + dispatch( + addAnswers({ + questionId: activeQuestion.question_id!, + answers: [target.value] as string[], + }), + ); + }; + + const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => { + if (!activeQuestion.question_id) return; + if (!user?.product_user_id) return; + + if (event.key === "Enter") { + dispatch( + saveAnswer({ + questionId: activeQuestion.question_id!, + bpuid: user.product_user_id, + }), + ); + } + }; + + return ( + <div className="px-6 my-6"> + <input + type="text" + id="text-entry-input" + aria-describedby="" + // defaultValue={answer?.values.length ? answer?.values[0] : ""} + onKeyUp={handleInputChange} + onKeyDown={handleKeyPress} // Use onKeyDown to listen for the key press + // title={error ? answer?.error_msg : ""} + className={clsx( + "w-full border-1 rounded px-2 py-1", + error + ? "border-red-500 focus-visible:ring-red-500" + : "border-blue-50", + )} + /> - if (event.key === 'Enter') { - dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) - } + {validation.errors.map((e) => { + return <p className="text-sm text-red-100 my-2">{e.message}</p>; + })} + </div> + ); +}; +export default function useKeyboardMode(rowModel: RowModel<UpkQuestionChoice>) { + // const [keyboardEnabled, setKeyboardEnabled] = useState( + // localStorage.getItem(keyboardMode) === "enabled", + // ); + const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]); - if (nextIdx !== prevIdx) { + const dispatch = useAppDispatch(); + const activeQuestion = useSelector(selectActiveQuestion); + const { data: user } = useGetMeQuery(); - rowRefs.current[prevIdx]?.classList.remove("bg-blue-400"); - rowRefs.current[nextIdx]?.classList.add("bg-blue-400"); + const keyboardSelectionIdxRef = useRef(0); - rowRefs.current[nextIdx]?.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - keyboardSelectionIdxRef.current = nextIdx; - } - }; + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (!activeQuestion) return; + if (!user?.product_user_id) return; - // I really don't know, and I don't really care enough to make this - // technically type'd correctly. - Max + // if (!keyboardEnabled) { + // return; + // } - // @ts-ignore - window.addEventListener("keydown", handleKeyDown); - // @ts-ignore - return () => window.removeEventListener("keydown", handleKeyDown); + const { key } = event; + const totalRows = rowRefs.current.length; + const prevIdx = keyboardSelectionIdxRef.current; + let nextIdx = prevIdx; - }, [rowModel.rows]); + if (key === "ArrowDown") nextIdx = Math.min(prevIdx + 1, totalRows - 1); + if (key === "ArrowUp") nextIdx = Math.max(prevIdx - 1, 0); + if (key === " ") { + const active = document.activeElement as HTMLElement | null; - return { - // keyboardEnabled, - // setKeyboardEnabled, - rowRefs, - keyboardSelectionIdxRef, + if (active?.tabIndex === 0) { + return; + } else { + rowModel.rows[keyboardSelectionIdxRef.current].toggleSelected(); + } + } + + if (event.key === "Enter") { + dispatch( + saveAnswer({ + questionId: activeQuestion.question_id!, + bpuid: user.product_user_id, + }), + ); + } + + if (nextIdx !== prevIdx) { + rowRefs.current[prevIdx]?.classList.remove("bg-blue-400"); + rowRefs.current[nextIdx]?.classList.add("bg-blue-400"); + + rowRefs.current[nextIdx]?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + keyboardSelectionIdxRef.current = nextIdx; + } }; + + // I really don't know, and I don't really care enough to make this + // technically type'd correctly. - Max + + // @ts-ignore + window.addEventListener("keydown", handleKeyDown); + // @ts-ignore + return () => window.removeEventListener("keydown", handleKeyDown); + }, [rowModel.rows]); + + return { + // keyboardEnabled, + // setKeyboardEnabled, + rowRefs, + keyboardSelectionIdxRef, + }; } const MultipleChoice = () => { - const dispatch = useAppDispatch() - const activeQuestion = useSelector(selectActiveQuestion); - - // const error: Boolean = answer?.error_msg.length > 0 - // console.log("Rendering MultipleChoice with answer:", answer, error) - - // TODO! Retrieve store values to use as the initial state for the table - // This is useful for if they tab around before submitting the answer - const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); - const [globalFilter, setGlobalFilter] = useState(''); - - const handleRowSelectionChange = (updaterOrValue: RowSelectionState | ((old: RowSelectionState) => RowSelectionState)) => { - setRowSelection((oldSelection) => { - // Get the new selection state - const newSelection = typeof updaterOrValue === 'function' - ? updaterOrValue(oldSelection) - : updaterOrValue; - - // Find which row was just selected/deselected - const changedRowId = Object.keys(newSelection).find( - key => newSelection[key] !== oldSelection[key] - ); - - if (!changedRowId) return newSelection; - - // Get the actual row data - const row = table.getRow(changedRowId); - const rowData = row.original; // Your row data object - - // Check if "None of the Above" was selected - const normalized = rowData.choice_text.trim().toLowerCase(); - const isNoneOfTheAbove = (normalized === "none of the above") || (rowData.exclusive ?? false); - if (isNoneOfTheAbove && newSelection[changedRowId]) { - // Clear all other selections, keep only this one - return { [changedRowId]: true }; - } - - // Check if a regular option was selected while "None of the Above" exists - const noneOfTheAboveId = Object.keys(newSelection).find(id => { - const r = table.getRow(id); - const rowData = r.original as UpkQuestionChoice; - - const normalized = rowData.choice_text.trim().toLowerCase(); - const isNoneOfTheAbove = (normalized === "none of the above") || (rowData.exclusive ?? false); - - return isNoneOfTheAbove && newSelection[id]; - }); - - if (noneOfTheAboveId && changedRowId !== noneOfTheAboveId) { - // Remove "None of the Above" selection - const { [noneOfTheAboveId]: _, ...rest } = newSelection; - return rest; - } - - return newSelection; - }); - }; + const dispatch = useAppDispatch(); + const activeQuestion = useSelector(selectActiveQuestion); + + // const error: Boolean = answer?.error_msg.length > 0 + // console.log("Rendering MultipleChoice with answer:", answer, error) + + // TODO! Retrieve store values to use as the initial state for the table + // This is useful for if they tab around before submitting the answer + const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); + const [globalFilter, setGlobalFilter] = useState(""); + + const handleRowSelectionChange = ( + updaterOrValue: + | RowSelectionState + | ((old: RowSelectionState) => RowSelectionState), + ) => { + setRowSelection((oldSelection) => { + // Get the new selection state + const newSelection = + typeof updaterOrValue === "function" + ? updaterOrValue(oldSelection) + : updaterOrValue; + + // Find which row was just selected/deselected + const changedRowId = Object.keys(newSelection).find( + (key) => newSelection[key] !== oldSelection[key], + ); + + if (!changedRowId) return newSelection; + + // Get the actual row data + const row = table.getRow(changedRowId); + const rowData = row.original; // Your row data object + + // Check if "None of the Above" was selected + const normalized = rowData.choice_text.trim().toLowerCase(); + const isNoneOfTheAbove = + normalized === "none of the above" || (rowData.exclusive ?? false); + if (isNoneOfTheAbove && newSelection[changedRowId]) { + // Clear all other selections, keep only this one + return { [changedRowId]: true }; + } + + // Check if a regular option was selected while "None of the Above" exists + const noneOfTheAboveId = Object.keys(newSelection).find((id) => { + const r = table.getRow(id); + const rowData = r.original as UpkQuestionChoice; + + const normalized = rowData.choice_text.trim().toLowerCase(); + const isNoneOfTheAbove = + normalized === "none of the above" || (rowData.exclusive ?? false); + + return isNoneOfTheAbove && newSelection[id]; + }); + + if (noneOfTheAboveId && changedRowId !== noneOfTheAboveId) { + // Remove "None of the Above" selection + const { [noneOfTheAboveId]: _, ...rest } = newSelection; + return rest; + } + + return newSelection; + }); + }; + + useEffect(() => { + if (!activeQuestion?.question_id) return; + // Run this anytime a rowSelection changes, to update + // the answer in the store + const selectedChoices = Object.keys(rowSelection); + dispatch( + addAnswers({ + questionId: activeQuestion.question_id, + answers: selectedChoices, + }), + ); + }, [rowSelection]); + + // Define columns + const columnHelper = createColumnHelper<UpkQuestionChoice>(); + const columns = [ + columnHelper.display({ + id: "select", + cell: ({ row }) => { + const canSelected = row.getCanSelect(); - useEffect(() => { - if (!activeQuestion?.question_id) return; - // Run this anytime a rowSelection changes, to update - // the answer in the store - const selectedChoices = Object.keys(rowSelection) - dispatch(addAnswers({ questionId: activeQuestion.question_id, answers: selectedChoices })) - }, [rowSelection]) - - // Define columns - const columnHelper = createColumnHelper<UpkQuestionChoice>() - const columns = [ - columnHelper.display({ - id: 'select', - cell: ({ row }) => { - const canSelected = row.getCanSelect() - - return ( - <input - type="checkbox" - checked={row.getIsSelected()} - disabled={!row.getCanSelect()} - onChange={row.getToggleSelectedHandler()} - className={`w-4 h-4 rounded-0 border-slate-300 cursor-pointer + return ( + <input + type="checkbox" + checked={row.getIsSelected()} + disabled={!row.getCanSelect()} + onChange={row.getToggleSelectedHandler()} + className={`w-4 h-4 rounded-0 border-slate-300 cursor-pointer focus:outline-none focus:border-blue-500 focus:ring-blue-500 - ${canSelected ? ' ' : 'hidden'} + ${canSelected ? " " : "hidden"} `} - /> - ) - }, - size: 20, - meta: { - align: 'center' - } - }), - - columnHelper.accessor("choice_text", { - header: () => 'Choice Text', - cell: (info) => { - const text = info.getValue(); - const selected = info.row.getIsSelected() - const canSelected = info.row.getCanSelect() + /> + ); + }, + size: 20, + meta: { + align: "center", + }, + }), + + columnHelper.accessor("choice_text", { + header: () => "Choice Text", + cell: (info) => { + const text = info.getValue(); + const selected = info.row.getIsSelected(); + const canSelected = info.row.getCanSelect(); - return ( - <p className={` + return ( + <p + className={` w-max truncate text-sm text-left pl-1 - ${selected ? 'font-bold' : 'font-medium'} - ${canSelected ? 'text-blue-950' : 'text-blue-600'} - `}> - {text} - </p> - ) - }, - size: 380, - meta: { - align: 'left' - } - }), - ] - - const table = useReactTable({ - 'data': activeQuestion!.choices ?? [], - columns, - getRowId: (row) => row.choice_id, - state: { - rowSelection, - globalFilter, - }, - // enableRowSelection: true, - enableRowSelection: (row) => { - // Allow deselection always, but limit new selections - const currentSelectionCount = Object.keys(rowSelection).length; - const isRowSelected = rowSelection[row.id]; - - const max_selection = activeQuestion?.selector === "SA" ? 1 : activeQuestion?.choices?.length ?? 10 - - // Allow if: row is already selected OR we haven't hit the limit - return isRowSelected || currentSelectionCount < max_selection; - }, - onRowSelectionChange: handleRowSelectionChange, - onGlobalFilterChange: setGlobalFilter, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - }); - - const selectedCount = table.getSelectedRowModel().rows.length; - - //const { rowRefs, setKeyboardEnabled, keyboardSelectionIdxRef } = useKeyboardMode(table.getRowModel()); - const { rowRefs } = useKeyboardMode(table.getRowModel()); - - return ( - <> - - {/* Controls */} - <div className="py-1"> - <div className="flex flex-wrap gap-2"> - - {/* Search */} - <div className="flex-1"> - <input - type="text" - placeholder="Search choices..." - value={globalFilter ?? ''} - onChange={e => setGlobalFilter(e.target.value)} - className="w-full px-4 py-1 + ${selected ? "font-bold" : "font-medium"} + ${canSelected ? "text-blue-950" : "text-blue-600"} + `} + > + {text} + </p> + ); + }, + size: 380, + meta: { + align: "left", + }, + }), + ]; + + const table = useReactTable({ + data: activeQuestion!.choices ?? [], + columns, + getRowId: (row) => row.choice_id, + state: { + rowSelection, + globalFilter, + }, + // enableRowSelection: true, + enableRowSelection: (row) => { + // Allow deselection always, but limit new selections + const currentSelectionCount = Object.keys(rowSelection).length; + const isRowSelected = rowSelection[row.id]; + + const max_selection = + activeQuestion?.selector === "SA" + ? 1 + : (activeQuestion?.choices?.length ?? 10); + + // Allow if: row is already selected OR we haven't hit the limit + return isRowSelected || currentSelectionCount < max_selection; + }, + onRowSelectionChange: handleRowSelectionChange, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }); + + const selectedCount = table.getSelectedRowModel().rows.length; + + //const { rowRefs, setKeyboardEnabled, keyboardSelectionIdxRef } = useKeyboardMode(table.getRowModel()); + const { rowRefs } = useKeyboardMode(table.getRowModel()); + + return ( + <> + {/* Controls */} + <div className="py-1"> + <div className="flex flex-wrap gap-2"> + {/* Search */} + <div className="flex-1"> + <input + type="text" + placeholder="Search choices..." + value={globalFilter ?? ""} + onChange={(e) => setGlobalFilter(e.target.value)} + className="w-full px-4 py-1 border border-blue-900 bg-blue-400 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" - /> - </div> + /> + </div> - {/* Selected Count */} - {selectedCount > 0 && ( - <div className="flex items-center gap-2 px-2 py-1 + {/* Selected Count */} + {selectedCount > 0 && ( + <div + className="flex items-center gap-2 px-2 py-1 border border-blue-900 text-blue-700 rounded font-medium text-[10px] hover:cursor-pointer hover:bg-blue-800 hover:text-white " - onClick={() => setRowSelection({})}> - <span>{selectedCount} selected</span> - <button className="m-0 p-0 font-bold">✕</button> - </div> - )} - - </div> + onClick={() => setRowSelection({})} + > + <span>{selectedCount} selected</span> + <button className="m-0 p-0 font-bold">✕</button> </div> + )} + </div> + </div> - {/* Table */} - <div className="border border-blue-200 rounded max-h-[400px] overflow-y-auto + {/* Table */} + <div + className="border border-blue-200 rounded max-h-[400px] overflow-y-auto [&::-webkit-scrollbar]:h-[4px] [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-track]:bg-blue-100 [&::-webkit-scrollbar-thumb]:bg-blue-300 - "> - <table className="text-xs table-fixed border-collapse w-full"> - <tbody className="w-full"> - {table.getRowModel().rows.map((row, idx) => ( - <tr key={row.id} - ref={(el) => { - rowRefs.current[idx] = el; - }} - onClick={() => row.toggleSelected()} - className={`cursor-pointer transition-colors w-full - ${row.getIsSelected() ? 'bg-blue-600 ' : ' ' + " + > + <table className="text-xs table-fixed border-collapse w-full"> + <tbody className="w-full"> + {table.getRowModel().rows.map((row, idx) => ( + <tr + key={row.id} + ref={(el) => { + rowRefs.current[idx] = el; + }} + onClick={() => row.toggleSelected()} + className={`cursor-pointer transition-colors w-full + ${ + row.getIsSelected() ? "bg-blue-600 " : " " }`} - > - - {row.getVisibleCells().map((cell) => { - const align = cell.column.columnDef.meta?.align || 'left'; - const alignClass = align === 'center' ? 'text-center' : - align === 'right' ? 'text-right' : 'text-left'; - - return ( - <td key={cell.id} - className={`py-2 ${alignClass}`} - style={{ - width: `${cell.column.getSize()}px`, - }} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - </td> - ) - })} - - </tr> - ))} - </tbody> - </table> - </div> - - {/* Empty State */} - {table.getRowModel().rows.length === 0 && ( - <div className="p-12 text-center text-blue-50 font-bold"> - No items found matching your search - </div> - )} + > + {row.getVisibleCells().map((cell) => { + const align = cell.column.columnDef.meta?.align || "left"; + const alignClass = + align === "center" + ? "text-center" + : align === "right" + ? "text-right" + : "text-left"; + + return ( + <td + key={cell.id} + className={`py-2 ${alignClass}`} + style={{ + width: `${cell.column.getSize()}px`, + }} + > + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + </td> + ); + })} + </tr> + ))} + </tbody> + </table> + </div> + + {/* Empty State */} + {table.getRowModel().rows.length === 0 && ( + <div className="p-12 text-center text-blue-50 font-bold"> + No items found matching your search + </div> + )} - {/* Footer */} - <div className="px-2 py-2 flex justify-between items-center + {/* Footer */} + <div + className="px-2 py-2 flex justify-between items-center text-xs font-mono font-semibold text-blue-950 - "> - <span> - Showing {table.getRowModel().rows.length} of {activeQuestion?.choices?.length} choices - </span> - {selectedCount > 0 && ( - <span> - {selectedCount} choice{selectedCount !== 1 ? 's' : ''} selected - </span> - )} - </div> - - </> - ) - -} + " + > + <span> + Showing {table.getRowModel().rows.length} of{" "} + {activeQuestion?.choices?.length} choices + </span> + {selectedCount > 0 && ( + <span> + {selectedCount} choice{selectedCount !== 1 ? "s" : ""} selected + </span> + )} + </div> + </> + ); +}; const ProfilingQuestionTitle = () => { - const activeQuestion = useSelector(selectActiveQuestion); - const question_active_tasks = activeQuestion?.importance?.task_count ?? 0 + const activeQuestion = useSelector(selectActiveQuestion); + const question_active_tasks = activeQuestion?.importance?.task_count ?? 0; - if (!activeQuestion) return null; - - let text = activeQuestion?.question_text + if (!activeQuestion) return null; - if (activeQuestion.explanation_template && activeQuestion._validation?.isComplete && activeQuestion._metadata.answers.length == 1) { - const template = activeQuestion.explanation_template; - const pre_code = activeQuestion._metadata.answers[0]; + let text = activeQuestion?.question_text; - const choice = activeQuestion.choices?.find(c => c.choice_id === pre_code) - const choice_text = choice ? choice.choice_text : "your answer" + if ( + activeQuestion.explanation_template && + activeQuestion._validation?.isComplete && + activeQuestion._metadata.answers.length == 1 + ) { + const template = activeQuestion.explanation_template; + const pre_code = activeQuestion._metadata.answers[0]; - text = template.replace("{answer}", choice_text) - } + const choice = activeQuestion.choices?.find( + (c) => c.choice_id === pre_code, + ); + const choice_text = choice ? choice.choice_text : "your answer"; + text = template.replace("{answer}", choice_text); + } - return ( - <div className="flex justify-between items-center px-2"> - - <h3 className="text-left text-sm font-bold m-0 p-0 + return ( + <div className="flex justify-between items-center px-2"> + <h3 + className="text-left text-sm font-bold m-0 p-0 inline-block - "> - {text} - </h3> - - <div className="max-w-[70px] text-center m-0 p-0"> - <h4 className="text-xs font-bold"> - {question_active_tasks.toLocaleString()} - </h4> - <h5 className="text-[10px]"> - Surveys use - this question - </h5> - </div> - </div> - ) -} + " + > + {text} + </h3> + + <div className="max-w-[70px] text-center m-0 p-0"> + <h4 className="text-xs font-bold"> + {question_active_tasks.toLocaleString()} + </h4> + <h5 className="text-[10px]">Surveys use this question</h5> + </div> + </div> + ); +}; const ProfilingQuestionSubmitButton = () => { - const dispatch = useAppDispatch() - - const activeQuestion = useSelector(selectActiveQuestion); - const activeQuestionValidation = useSelector(selectActiveQuestionValidation) - const bpuid = useAppSelector(state => state.app.bpuid) - - const handleOnClick = () => { - if (!activeQuestion) return - if (!activeQuestionValidation?.isComplete) return - if (!bpuid) return; - - dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) - } - - return ( - <button - type="submit" - className="cursor-pointer text-sm font-semibold + const dispatch = useAppDispatch(); + + const activeQuestion = useSelector(selectActiveQuestion); + const activeQuestionValidation = useSelector(selectActiveQuestionValidation); + const { data: user } = useGetMeQuery(); + + const handleOnClick = () => { + if (!activeQuestion) return; + if (!activeQuestionValidation?.isComplete) return; + if (!user?.product_user_id) return; + + dispatch( + saveAnswer({ + questionId: activeQuestion.question_id!, + bpuid: user.product_user_id, + }), + ); + }; + + return ( + <button + type="submit" + className="cursor-pointer text-sm font-semibold bg-blue-700 text-white py-1 px-4 rounded hover:bg-blue-900 min-w-[148px] disabled:bg-blue-700 disabled:text-rose-300 disabled:cursor-not-allowed " - disabled={!activeQuestionValidation?.isComplete} - onClick={handleOnClick} - > - Answer Question - </button> - ) -} - + disabled={!activeQuestionValidation?.isComplete} + onClick={handleOnClick} + > + Answer Question + </button> + ); +}; export const ProfileQuestionFull = () => { - const activeQuestion = useSelector(selectActiveQuestion); - - const renderContent = () => { - if (!activeQuestion) return null; - switch (activeQuestion.question_type) { - case 'TE': - return <TextEntry /> - case 'MC': - return <MultipleChoice /> - default: - return <MultipleChoice /> - } - }; + const activeQuestion = useSelector(selectActiveQuestion); - if (!activeQuestion) return null + const renderContent = () => { + if (!activeQuestion) return null; + switch (activeQuestion.question_type) { + case "TE": + return <TextEntry />; + case "MC": + return <MultipleChoice />; + default: + return <MultipleChoice />; + } + }; - return ( - <> - {/* {answer && answer.processing && ( + if (!activeQuestion) return null; + + return ( + <> + {/* {answer && answer.processing && ( <motion.div className="bg-gray-300" initial={{ width: "0%" }} @@ -540,104 +576,111 @@ export const ProfileQuestionFull = () => { /> )} */} - {renderContent()} - </> - ) -} + {renderContent()} + </> + ); +}; const PaginationIcon: React.FC<{ - question: ProfileQuestion, idx: number, + question: ProfileQuestion; + idx: number; }> = ({ question, idx }) => { - const dispatch = useAppDispatch() + const dispatch = useAppDispatch(); - // const answers = useAppSelector(state => state.answers) - const isActive = question._metadata?.isActive ?? false - const completed = question._validation?.isComplete ?? false + // const answers = useAppSelector(state => state.answers) + const isActive = question._metadata?.isActive ?? false; + const completed = question._validation?.isComplete ?? false; - const handleSetQuestion = (evt: MouseEvent<HTMLAnchorElement>) => { - if (completed) { - evt.preventDefault() - } else { - dispatch(setActiveQuestion(question.question_id!)) - } + const handleSetQuestion = (evt: MouseEvent<HTMLAnchorElement>) => { + if (completed) { + evt.preventDefault(); + } else { + dispatch(setActiveQuestion(question.question_id!)); } - - return ( - <PaginationItem> - <PaginationLink - href="#" - title={question.question_text} - isActive={isActive} - aria-disabled={!!completed} - - onClick={handleSetQuestion} - className={clsx("cursor-pointer rounded border border-blue-300 text-blue-300 text-[10px] w-7 h-4", - { - "pointer-events-none cursor-not-allowed": completed, - "bg-blue-800 border-blue-800": isActive, - })} - > - {idx + 1} - </PaginationLink> - </PaginationItem> - ) -} + }; + + return ( + <PaginationItem> + <PaginationLink + href="#" + title={question.question_text} + isActive={isActive} + aria-disabled={!!completed} + onClick={handleSetQuestion} + className={clsx( + "cursor-pointer rounded border border-blue-300 text-blue-300 text-[10px] w-7 h-4", + { + "pointer-events-none cursor-not-allowed": completed, + "bg-blue-800 border-blue-800": isActive, + }, + )} + > + {idx + 1} + </PaginationLink> + </PaginationItem> + ); +}; const ProfilingQuestionPagination = () => { - const dispatch = useAppDispatch() - - const questions = useSelector(selectAllQuestions) - const activeQuestion = useSelector(selectActiveQuestion) - - // All the variables needed for a sliding window - const q_idx = questions.findIndex(q => q.question_id === activeQuestion!.question_id) - - const handleNextClick = () => { - dispatch(goToNextQuestion()); - }; - - const questionsWindow = ( - items: ProfileQuestion[], currentIndex: number, windowSize: number = 5 - ): ProfileQuestion[] => { - const half: number = Math.floor(windowSize / 2) - const total: number = items.length - let start: number = currentIndex - half - let end: number = currentIndex + half + 1 - - if (start < 0) { - end += Math.abs(start) - start = 0 - } - - // Adjust if window goes past the end - if (end > total) { - const overflow: number = end - total - start = Math.max(0, start - overflow) - end = total - } + const dispatch = useAppDispatch(); + + const questions = useSelector(selectAllQuestions); + const activeQuestion = useSelector(selectActiveQuestion); + + // All the variables needed for a sliding window + const q_idx = questions.findIndex( + (q) => q.question_id === activeQuestion!.question_id, + ); + + const handleNextClick = () => { + dispatch(goToNextQuestion()); + }; + + const questionsWindow = ( + items: ProfileQuestion[], + currentIndex: number, + windowSize: number = 5, + ): ProfileQuestion[] => { + const half: number = Math.floor(windowSize / 2); + const total: number = items.length; + let start: number = currentIndex - half; + let end: number = currentIndex + half + 1; + + if (start < 0) { + end += Math.abs(start); + start = 0; + } - return items.slice(start, end) + // Adjust if window goes past the end + if (end > total) { + const overflow: number = end - total; + start = Math.max(0, start - overflow); + end = total; } - return ( - <Pagination> - <PaginationContent> - { - questionsWindow(questions, q_idx).map(q => { - return <PaginationIcon - key={q.question_id} - question={q} - idx={questions.findIndex(qq => qq.question_id === q.question_id)} - /> - }) - } - - <PaginationItem> - <PaginationLink - aria-label="Go to next page" - size="default" - - className="rounded text-[10px] p-0 m-0 + return items.slice(start, end); + }; + + return ( + <Pagination> + <PaginationContent> + {questionsWindow(questions, q_idx).map((q) => { + return ( + <PaginationIcon + key={q.question_id} + question={q} + idx={questions.findIndex( + (qq) => qq.question_id === q.question_id, + )} + /> + ); + })} + + <PaginationItem> + <PaginationLink + aria-label="Go to next page" + size="default" + className="rounded text-[10px] p-0 m-0 text-blue-300 border border-blue-100 text-blue-100 hover:bg-blue-800 hover:border-blue-800 hover:text-white @@ -645,115 +688,111 @@ const ProfilingQuestionPagination = () => { h-4 w-13 sm:w-14 " - onClick={handleNextClick} - href="#" - > - <span className="block">Next</span> - <ChevronRightIcon /> - </PaginationLink> - - </PaginationItem> - </PaginationContent> - </Pagination> - ) -} + onClick={handleNextClick} + href="#" + > + <span className="block">Next</span> + <ChevronRightIcon /> + </PaginationLink> + </PaginationItem> + </PaginationContent> + </Pagination> + ); +}; const ProfilingHeader = () => { - const questions = useSelector(questionsSelectors.selectAll); + const questions = useSelector(questionsSelectors.selectAll); - return ( - <h2 className="text-xs font-mono font-bold p-0 m-0"> - {questions.length} Profiling Questions - </h2> - ) -} + return ( + <h2 className="text-xs font-mono font-bold p-0 m-0"> + {questions.length} Profiling Questions + </h2> + ); +}; const ProfilingModalBody = () => { - const activeQuestion = useSelector(selectActiveQuestion); - - if (!activeQuestion) { - return ( - <div className="p-5"> - <h3 className="text-lg font-bold">No Profiling Questions Available</h3> - <p className="my-0 text-sm text-gray-600">Please check back later.</p> - </div> - ) - } + const activeQuestion = useSelector(selectActiveQuestion); + if (!activeQuestion) { return ( - <div className="relative flex flex-col"> - {/* <ProfilingQuestionPagination /> */} - - <div className="flex-shrink-0"> - <ProfilingQuestionTitle /> - </div> - - {/* Scrollable Content */} - <div className="flex-1 p-1"> - <ProfileQuestionFull key={activeQuestion.question_id} /> - </div> - - {/* Sticky Footer */} - <div className="flex-shrink-0 bg-blue-600 p-1 flex justify-between - "> - <div className="justify-start"> - <ProfilingQuestionPagination /> - </div> - <div className="justify-end"> - <ProfilingQuestionSubmitButton /> - </div> - </div> - + <div className="p-5"> + <h3 className="text-lg font-bold">No Profiling Questions Available</h3> + <p className="my-0 text-sm text-gray-600">Please check back later.</p> + </div> + ); + } + + return ( + <div className="relative flex flex-col"> + {/* <ProfilingQuestionPagination /> */} + + <div className="flex-shrink-0"> + <ProfilingQuestionTitle /> + </div> + + {/* Scrollable Content */} + <div className="flex-1 p-1"> + <ProfileQuestionFull key={activeQuestion.question_id} /> + </div> + + {/* Sticky Footer */} + <div + className="flex-shrink-0 bg-blue-600 p-1 flex justify-between + " + > + <div className="justify-start"> + <ProfilingQuestionPagination /> </div> - ) -} + <div className="justify-end"> + <ProfilingQuestionSubmitButton /> + </div> + </div> + </div> + ); +}; const Profiling = () => { - /* This is the main UI for any User Profiling (before entering a task). It's + /* This is the main UI for any User Profiling (before entering a task). It's primary window and position should be dedictated by it's parent so this can be positioned as a window or a sidebar as needed. */ - const [showProfiling, setShowProfiling] = useState(false); + const [showProfiling, setShowProfiling] = useState(false); - const dispatch = useAppDispatch() - const bpuid = useAppSelector(state => state.app.bpuid) + const dispatch = useAppDispatch(); + const { data: user } = useGetMeQuery(); - useEffect(() => { - if (!bpuid) return; - dispatch(fetchNewQuestions(bpuid)); - }, [bpid, bpuid]) + useEffect(() => { + if (!user?.product_user_id) return; + dispatch(fetchNewQuestions(user.product_user_id)); + }, [bpid, user?.product_user_id]); - if (!bpuid) return null; + if (!user?.product_user_id) return null; - return ( - <div className={clsx( - "fixed rounded bg-blue-500 text-white shadow-lg z-60 \ + return ( + <div + className={clsx( + "fixed rounded bg-blue-500 text-white shadow-lg z-60 \ bottom-1 sm:bottom-6 \ left-1 sm:left-2", - showProfiling ? "w-[98vw] sm:w-[400px]" : "w-[220px]" - )} - > - - <div className="bg-blue-600 rounded border-white-300 + showProfiling ? "w-[98vw] sm:w-[400px]" : "w-[220px]", + )} + > + <div + className="bg-blue-600 rounded border-white-300 cursor-pointer py-1 px-4 hover:bg-blue-700 transition-colors duration-200" - onClick={() => setShowProfiling(!showProfiling)} - > - <ProfilingHeader /> - - <h4 className="text-[10px] text-center italic text-white-300 py-0"> - (Click to {showProfiling ? 'hide' : 'show'}) - </h4> - </div> - - {showProfiling && ( - <ProfilingModalBody /> - )} - - </div> - ) -} - -export { - Profiling + onClick={() => setShowProfiling(!showProfiling)} + > + <ProfilingHeader /> + + <h4 className="text-[10px] text-center italic text-white-300 py-0"> + (Click to {showProfiling ? "hide" : "show"}) + </h4> + </div> + + {showProfiling && <ProfilingModalBody />} + </div> + ); }; + +export { Profiling }; diff --git a/jb-ui/src/components/Redemption.tsx b/jb-ui/src/components/Redemption.tsx new file mode 100644 index 0000000..1d76f17 --- /dev/null +++ b/jb-ui/src/components/Redemption.tsx @@ -0,0 +1,61 @@ +import { clsx } from "clsx"; +import { useState } from "react"; +// import { useSelector } from "react-redux"; +// import { cashoutMethodsSelectors } from "@/models/cashoutMethodsSlice"; + +const RedemptionHeader = () => { + // const cashout_methods = useSelector(cashoutMethodsSelectors.selectAll); + + return ( + <> + <h2 className="text-xs font-mono font-bold p-0 m-0"> + {/* {cashout_methods.length} Redemption Methods */} + 30+ Redemption Methods + </h2> + </> + ); +}; + +const RedemptionModalBody = () => { + return ( + <div className="grid grid-cols-[auto_1fr] text-xs"> + <div className="label text-center p-2"> + Paypal, Visa, Giftcards will be available. Finishing processing the + transfer from Amazon. + </div> + </div> + ); +}; + +const Redemption = () => { + const [showRedemption, setShowRedemption] = useState(false); + + return ( + <div + className={clsx( + "fixed rounded bg-blue-500 text-white shadow-lg z-60 \ + top-1 sm:top-6 \ + right-1 sm:right-2", + showRedemption ? "w-[98vw] sm:w-[400px]" : "w-[220px]", + )} + > + <div + className="bg-blue-600 rounded border-white-300 + cursor-pointer py-1 px-4 + hover:bg-blue-700 transition-colors duration-200" + onClick={() => setShowRedemption(!showRedemption)} + > + <RedemptionHeader /> + + <h4 className="text-[10px] text-center italic text-white-300 py-0"> + {/* (Click to {showRedemption ? "hide" : "show"}) */} + Details within 24hr. + </h4> + </div> + + {showRedemption && <RedemptionModalBody />} + </div> + ); +}; + +export default Redemption; diff --git a/jb-ui/src/components/Wallet.tsx b/jb-ui/src/components/Wallet.tsx index e4fc694..819eff7 100644 --- a/jb-ui/src/components/Wallet.tsx +++ b/jb-ui/src/components/Wallet.tsx @@ -1,464 +1,498 @@ import { - UserLedgerTransactionsResponse, - UserLedgerTransactionsResponseTransactionsInner, - WalletApi + UserLedgerTransactionsResponse, + UserLedgerTransactionsResponseTransactionsInner, + WalletApi, } from "@/api_fsb"; +import { useGetMeQuery } from "@/models/authAPI"; import { useAppDispatch, useAppSelector } from "@/hooks"; -import { bpid, formatCentsToUSD, } from "@/lib/utils"; +import { bpid, formatCentsToUSD } from "@/lib/utils"; import { - setTxPagination, setTxTotalItems, setTxTotalPages, - setUserLedgerSummary, - setUserLedgerTxs + setTxPagination, + setTxTotalItems, + setTxTotalPages, + setUserLedgerSummary, + setUserLedgerTxs, } from "@/models/appSlice"; import { - RowData, createColumnHelper, flexRender, getCoreRowModel, - useReactTable + RowData, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, } from "@tanstack/react-table"; import moment from "moment"; import { useEffect, useState } from "react"; -declare module '@tanstack/react-table' { - interface ColumnMeta<TData extends RowData, TValue> { - align?: 'left' | 'center' | 'right'; - } +declare module "@tanstack/react-table" { + interface ColumnMeta<TData extends RowData, TValue> { + align?: "left" | "center" | "right"; + } } // Function to calculate background color based on balance const getBackgroundColor = (balance: number | undefined): string => { - if (balance === undefined || balance === null) return 'bg-blue-600'; - - if (balance > 0) { - return 'bg-blue-600'; - } else if (balance <= -100) { - return 'bg-red-600'; - } else { - // Interpolate between blue and red for values between 0 and -100 - // progress goes from 0 (at balance = 0) to 1 (at balance = -100) - const progress = Math.abs(balance) / 100; - - // Blue RGB: (37, 99, 235) - Tailwind blue-600 - // Red RGB: (220, 38, 38) - Tailwind red-600 - const r = Math.round(37 + (220 - 37) * progress); - const g = Math.round(99 + (38 - 99) * progress); - const b = Math.round(235 + (38 - 235) * progress); - - return `rgb(${r}, ${g}, ${b})`; - } + if (balance === undefined || balance === null) return "bg-blue-600"; + + if (balance > 0) { + return "bg-blue-600"; + } else if (balance <= -100) { + return "bg-red-600"; + } else { + // Interpolate between blue and red for values between 0 and -100 + // progress goes from 0 (at balance = 0) to 1 (at balance = -100) + const progress = Math.abs(balance) / 100; + + // Blue RGB: (37, 99, 235) - Tailwind blue-600 + // Red RGB: (220, 38, 38) - Tailwind red-600 + const r = Math.round(37 + (220 - 37) * progress); + const g = Math.round(99 + (38 - 99) * progress); + const b = Math.round(235 + (38 - 235) * progress); + + return `rgb(${r}, ${g}, ${b})`; + } }; - const WalletHeader = () => { - const amount = useAppSelector(state => state.app.userWalletBalance)?.amount ?? 0 - - const bgStyle = typeof getBackgroundColor(amount) === 'string' && - getBackgroundColor(amount).startsWith('bg-') - ? {} - : { backgroundColor: getBackgroundColor(amount) }; - - const bgClass = typeof getBackgroundColor(amount) === 'string' && - getBackgroundColor(amount).startsWith('bg-') - ? getBackgroundColor(amount) - : ''; - - return ( - <div className={`w-full py-1 px-1 text-white rounded ${bgClass}`} - style={bgStyle} - > - <h2 className="text-xs font-semibold flex justify-between items-center"> - <span className="text-left">User Wallet:</span> - - {amount >= 0 ? ( - <span className='text-emerald-50'> - {`${formatCentsToUSD(Math.abs(amount))}`} - </span> - ) : ( - <span className='text-rose-50'> - {`(${formatCentsToUSD(Math.abs(amount))})`} - </span> - )} - - </h2> - </div> - ) + const amount = + useAppSelector((state) => state.app.userDisplayedWalletBalance?.amount) ?? + 0; + + const amount_string = + useAppSelector( + (state) => state.app.userDisplayedWalletBalance?.amount_string, + ) ?? "$0.00"; + + const bgStyle = + typeof getBackgroundColor(amount) === "string" && + getBackgroundColor(amount).startsWith("bg-") + ? {} + : { backgroundColor: getBackgroundColor(amount) }; + + const bgClass = + typeof getBackgroundColor(amount) === "string" && + getBackgroundColor(amount).startsWith("bg-") + ? getBackgroundColor(amount) + : ""; + + return ( + <div + className={`w-full py-1 px-1 text-white rounded ${bgClass}`} + style={bgStyle} + > + <h2 className="text-xs font-semibold flex justify-between items-center"> + <span className="text-left">User Wallet:</span> + + {amount >= 0 ? ( + <span className="text-emerald-50">{`${amount_string}`}</span> + ) : ( + <span className="text-rose-50">{`(${amount_string})`}</span> + )} + </h2> + </div> + ); }; const WalletSummary = () => { - const summary = useAppSelector(state => state.app.userLedgerSummary) - - const survey_completes = summary?.bp_payment?.entry_count ?? 0 - const survey_completes_f = (survey_completes ?? 0).toLocaleString('en-US') - - const survey_adjustments = summary?.bp_adjustment?.entry_count ?? 0 - const survey_adjustments_f = (survey_adjustments ?? 0).toLocaleString('en-US') - - const min_payout: number = summary?.bp_payment?.min_amount ?? 0 - const max_payout: number = summary?.bp_payment?.max_amount ?? 0 - const adj_total: number = summary?.bp_adjustment?.total_amount ?? 0 - const user_payments_total: number = (summary?.user_payout_request?.total_amount ?? 0) * -1 - - return ( - <div className="grid grid-cols-[auto_1fr] text-xs"> - <div className="label text-left">Completes:</div> - <div className="value text-right">{survey_completes_f} surveys</div> - - <div className="label text-left">Avg Payouts:</div> - <div className="value text-right">{formatCentsToUSD(min_payout)} – {formatCentsToUSD(max_payout)}</div> - - <div className="label text-left">Survey Adjustments:</div> - <div className="value text-right">{survey_adjustments_f}</div> - - <div className="label text-left">Adjustment Amount:</div> - <div className="value text-right"> - {adj_total >= 0 ? ( - <span className='text-emerald-300'> - {`${formatCentsToUSD(adj_total)}`} - </span> - ) : ( - <span className='text-rose-300'> - {`(${formatCentsToUSD(Math.abs(adj_total))})`} - </span> - )} - </div> - - <div className="label text-left">User Payments:</div> - <div className="value text-right"> - {user_payments_total >= 0 ? ( - <span className='text-emerald-300'> - {`${formatCentsToUSD(user_payments_total)}`} - </span> - ) : ( - <span className='text-rose-300'> - {`(${formatCentsToUSD(Math.abs(user_payments_total))})`} - </span> - )} - - </div> - </div > - ) + const summary = useAppSelector((state) => state.app.userLedgerSummary); + + const userWallet = useAppSelector((state) => state.app.userWallet); + + userWallet; + + const survey_completes = summary?.bp_payment?.entry_count ?? 0; + const survey_completes_f = (survey_completes ?? 0).toLocaleString("en-US"); + + const survey_adjustments = summary?.bp_adjustment?.entry_count ?? 0; + const survey_adjustments_f = (survey_adjustments ?? 0).toLocaleString( + "en-US", + ); + + const min_payout: number = summary?.bp_payment?.min_amount ?? 0; + const max_payout: number = summary?.bp_payment?.max_amount ?? 0; + const adj_total: number = summary?.bp_adjustment?.total_amount ?? 0; + const user_payments_total: number = + (summary?.user_payout_request?.total_amount ?? 0) * -1; + + return ( + <div className="grid grid-cols-[auto_1fr] text-xs"> + <div className="label text-left">Completes:</div> + <div className="value text-right">{survey_completes_f} surveys</div> + + <div className="label text-left">Avg Payouts:</div> + <div className="value text-right"> + {formatCentsToUSD(min_payout)} – {formatCentsToUSD(max_payout)} + </div> + + <div className="label text-left">Survey Adjustments:</div> + <div className="value text-right">{survey_adjustments_f}</div> + + <div className="label text-left">Adjustment Amount:</div> + <div className="value text-right"> + {adj_total >= 0 ? ( + <span className="text-emerald-300"> + {`${formatCentsToUSD(adj_total)}`} + </span> + ) : ( + <span className="text-rose-300"> + {`(${formatCentsToUSD(Math.abs(adj_total))})`} + </span> + )} + </div> + + <div className="label text-left">User Payments:</div> + <div className="value text-right"> + {user_payments_total >= 0 ? ( + <span className="text-emerald-300"> + {`${formatCentsToUSD(user_payments_total)}`} + </span> + ) : ( + <span className="text-rose-300"> + {`(${formatCentsToUSD(Math.abs(user_payments_total))})`} + </span> + )} + </div> + </div> + ); }; const WalletTransactionsHeader = () => { - const tx_cnt = useAppSelector(state => state.app.txTotalItems) - const txt_cnt_f = (tx_cnt ?? 0).toLocaleString('en-US') - - const [showTx, setShowTx] = useState(false); - - return ( - <div className="w-full bg-blue-600 text-white rounded text-left"> - <div className="hover:cursor-pointer hover:bg-blue-700 transition-colors duration-200" - onClick={() => setShowTx(!showTx)}> - - <h2 className="text-xs font-semibold rounded flex justify-between + const tx_cnt = useAppSelector((state) => state.app.txTotalItems); + const txt_cnt_f = (tx_cnt ?? 0).toLocaleString("en-US"); + + const [showTx, setShowTx] = useState(false); + + return ( + <div className="w-full bg-blue-600 text-white rounded text-left"> + <div + className="hover:cursor-pointer hover:bg-blue-700 transition-colors duration-200" + onClick={() => setShowTx(!showTx)} + > + <h2 + className="text-xs font-semibold rounded flex justify-between py-1 px-1" - onClick={() => setShowTx(!showTx)} - > - <span>Total Transactions:</span> <span>{txt_cnt_f}</span> - </h2> - - <h4 className="text-[10px] text-center italic text-blue-300 py-1" - onClick={() => setShowTx(!showTx)} - > - (Click to {showTx ? 'hide' : 'show'}) - </h4> - - </div> - - {showTx && ( - <> - <WalletTransactions /> - </> - )} + onClick={() => setShowTx(!showTx)} + > + <span>Total Transactions:</span> <span>{txt_cnt_f}</span> + </h2> - </div> - ) + <h4 + className="text-[10px] text-center italic text-blue-300 py-1" + onClick={() => setShowTx(!showTx)} + > + (Click to {showTx ? "hide" : "show"}) + </h4> + </div> + + {showTx && ( + <> + <WalletTransactions /> + </> + )} + </div> + ); }; - const WalletTransactions = () => { - const dispatch = useAppDispatch() - const bpuid = useAppSelector(state => state.app.bpuid) - const userLedgerTxs = useAppSelector(state => state.app.userLedgerTxs); - - const txPagination = useAppSelector(state => state.app.txPagination); - const txTotalPages = useAppSelector(state => state.app.txTotalPages); - - useEffect(() => { - if (!bpuid) return; - - new WalletApi().getUserTransactionHistoryProductIdTransactionHistoryGet( - bpid, // productId - bpuid, // bpuid - undefined, // createdAfter - undefined, // createdBefore - "-created", // orderBy - txPagination.pageIndex + 1, // page - txPagination.pageSize // pageSize - ).then(res => { - const response = res.data as UserLedgerTransactionsResponse; - console.log("Wallet: fetched user ledger", response); - dispatch(setUserLedgerSummary(response.summary)); - - dispatch(setUserLedgerTxs(response.transactions ?? [])) - dispatch(setTxTotalItems(response.total!)) - dispatch(setTxTotalPages(response.pages!)) - }); - - }, [bpuid, txPagination.pageIndex, txPagination.pageSize]); - - const columnHelper = createColumnHelper<UserLedgerTransactionsResponseTransactionsInner>() - const columns = [ - columnHelper.accessor('created', { - header: () => 'Date', - cell: (info) => moment(info.getValue()).format('M/D/YY H:mm'), - size: 110, - meta: { - align: 'left' - } - }), - columnHelper.accessor('description', { - header: () => 'Type', - cell: props => { - const desc = props.getValue(); - switch (desc) { - case 'Task Complete': - return 'Survey 🎉'; - case 'Task Adjustment': - return 'Reject ⚠️'; - case 'Compensation Bonus': - return 'Bonus 🎁'; - case 'HIT Bonus': - return 'Bonus'; - case 'HIT Reward': - return 'Assignment'; - default: - return desc; - } - }, - size: 80, - meta: { - align: 'left' - } - }), - columnHelper.accessor('amount', { - header: () => 'Amount', - cell: (props) => { - const val = props.renderValue() as number; - const isPositive = val >= 0; - const emoji = isPositive ? "⬆\uFE0E" : "⬇\uFE0E"; - const colorClass = isPositive ? 'font-variant-emoji-text text-emerald-300' : 'font-variant-emoji-text text-rose-300'; + const dispatch = useAppDispatch(); + const { data: user } = useGetMeQuery(); + const userLedgerTxs = useAppSelector((state) => state.app.userLedgerTxs); + + const txPagination = useAppSelector((state) => state.app.txPagination); + const txTotalPages = useAppSelector((state) => state.app.txTotalPages); + + useEffect(() => { + if (!user?.product_user_id) return; + + new WalletApi() + .getUserTransactionHistoryProductIdTransactionHistoryGet( + bpid, // productId + user.product_user_id, + undefined, // createdAfter + undefined, // createdBefore + "-created", // orderBy + txPagination.pageIndex + 1, // page + txPagination.pageSize, // pageSize + ) + .then((res) => { + const response = res.data as UserLedgerTransactionsResponse; + console.log("Wallet: fetched user ledger", response); + dispatch(setUserLedgerSummary(response.summary)); + + dispatch(setUserLedgerTxs(response.transactions ?? [])); + dispatch(setTxTotalItems(response.total!)); + dispatch(setTxTotalPages(response.pages!)); + }); + }, [user?.product_user_id, txPagination.pageIndex, txPagination.pageSize]); + + const columnHelper = + createColumnHelper<UserLedgerTransactionsResponseTransactionsInner>(); + const columns = [ + columnHelper.accessor("created", { + header: () => "Date", + cell: (info) => moment(info.getValue()).format("M/D/YY H:mm"), + size: 110, + meta: { + align: "left", + }, + }), + columnHelper.accessor("description", { + header: () => "Type", + cell: (props) => { + const desc = props.getValue(); + switch (desc) { + case "Task Complete": + return "Survey 🎉"; + case "Task Adjustment": + return "Reject ⚠️"; + case "Compensation Bonus": + return "Bonus 🎁"; + case "HIT Bonus": + return "Bonus"; + case "HIT Reward": + return "Assignment"; + default: + return desc; + } + }, + size: 80, + meta: { + align: "left", + }, + }), + columnHelper.accessor("amount", { + header: () => "Amount", + cell: (props) => { + const val = props.renderValue() as number; + const isPositive = val >= 0; + const emoji = isPositive ? "⬆\uFE0E" : "⬇\uFE0E"; + const colorClass = isPositive + ? "font-variant-emoji-text text-emerald-300" + : "font-variant-emoji-text text-rose-300"; + + return ( + <span className={colorClass}> + {`${emoji} ${formatCentsToUSD(Math.abs(val))}`} + </span> + ); + }, + size: 70, + meta: { + align: "center", + }, + }), + columnHelper.accessor("balance_after", { + header: () => "Balance", + cell: (props) => { + const val = props.renderValue() as number; + const isPositive = val >= 0; + const colorClass = isPositive ? "text-emerald-300" : "text-rose-300"; + + return ( + <> + {isPositive ? ( + <span className={colorClass}> + {`${formatCentsToUSD(Math.abs(val))}`} + </span> + ) : ( + <span className={colorClass}> + {`(${formatCentsToUSD(Math.abs(val))})`} + </span> + )} + </> + ); + }, + size: 70, + meta: { + align: "center", + }, + }), + ]; + + const table = useReactTable({ + data: userLedgerTxs, + columns: columns, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: txTotalPages, + onPaginationChange: (updater) => { + dispatch( + setTxPagination( + typeof updater === "function" ? updater(txPagination) : updater, + ), + ); + }, + state: { + pagination: txPagination, + }, + }); + + return ( + <div className="w-full border-t-1 border-blue-800 p-2"> + <table className="text-xs table-fixed border-collapse"> + <thead className="font-bold"> + {table.getHeaderGroups().map((headerGroup) => ( + <tr key={headerGroup.id}> + {headerGroup.headers.map((header) => { + const align = header.column.columnDef.meta?.align || "left"; + const alignClass = + align === "center" + ? "text-center" + : align === "right" + ? "text-right" + : "text-left"; return ( - <span className={colorClass}> - {`${emoji} ${formatCentsToUSD(Math.abs(val))}`} - </span> - ) - }, - size: 70, - meta: { - align: 'center' - } - }), - columnHelper.accessor('balance_after', { - header: () => 'Balance', - cell: (props) => { - const val = props.renderValue() as number; - const isPositive = val >= 0; - const colorClass = isPositive ? 'text-emerald-300' : 'text-rose-300'; + <th + key={header.id} + className={`p-0 m-0 ${alignClass}`} + style={{ + width: `${header.column.getSize()}px`, + }} + > + {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + </th> + ); + })} + </tr> + ))} + </thead> + <tbody> + {table.getRowModel().rows.map((row) => ( + <tr key={row.id}> + {row.getVisibleCells().map((cell) => { + const align = cell.column.columnDef.meta?.align || "left"; + const alignClass = + align === "center" + ? "text-center" + : align === "right" + ? "text-right" + : "text-left"; return ( - <> - {isPositive ? ( - <span className={colorClass}> - {`${formatCentsToUSD(Math.abs(val))}`} - </span> - ) : ( - <span className={colorClass}> - {`(${formatCentsToUSD(Math.abs(val))})`} - </span> - )} - </> - ) - }, - size: 70, - meta: { - align: 'center' - } - }), - ] - - const table = useReactTable({ - 'data': userLedgerTxs, - 'columns': columns, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - pageCount: txTotalPages, - onPaginationChange: (updater) => { - dispatch(setTxPagination( - typeof updater === 'function' ? updater(txPagination) : updater - )); - }, - state: { - pagination: txPagination, - }, - }); - - return ( - <div className="w-full border-t-1 border-blue-800 p-2"> - <table className="text-xs table-fixed border-collapse"> - <thead className="font-bold"> - {table.getHeaderGroups().map((headerGroup) => ( - - <tr key={headerGroup.id}> - {headerGroup.headers.map((header) => { - const align = header.column.columnDef.meta?.align || 'left'; - const alignClass = align === 'center' ? 'text-center' : align === 'right' ? 'text-right' : 'text-left'; - - return ( - <th - key={header.id} - className={`p-0 m-0 ${alignClass}`} - style={{ - width: `${header.column.getSize()}px`, - }} - > - {flexRender( - header.column.columnDef.header, - header.getContext(), - )} - </th> - ) - })} - </tr> - ))} - </thead> - <tbody> - {table.getRowModel().rows.map((row) => ( - <tr key={row.id}> - {row.getVisibleCells().map((cell) => { - const align = cell.column.columnDef.meta?.align || 'left'; - const alignClass = align === 'center' ? 'text-center' : - align === 'right' ? 'text-right' : 'text-left'; - - return ( - <td key={cell.id} - className={`p-0 ${alignClass}`} - style={{ - width: `${cell.column.getSize()}px`, - }} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - </td> - ) - })} - </tr> - ))} - </tbody> - - <tfoot> - <tr> - <td colSpan={columns.length} className="p-2"> - <div className="flex items-center justify-between text-xs"> - - {/* Page info */} - <div className="text-blue-50"> - Page {table.getState().pagination.pageIndex + 1} of{' '} - {table.getPageCount()} - </div> - - {/* Navigation buttons */} - <div className="flex gap-8 text-xs"> - <button - onClick={() => table.previousPage()} - disabled={!table.getCanPreviousPage()} - className="px-3 py-1 border border-blue-200 rounded + <td + key={cell.id} + className={`p-0 ${alignClass}`} + style={{ + width: `${cell.column.getSize()}px`, + }} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </td> + ); + })} + </tr> + ))} + </tbody> + + <tfoot> + <tr> + <td colSpan={columns.length} className="p-2"> + <div className="flex items-center justify-between text-xs"> + {/* Page info */} + <div className="text-blue-50"> + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + </div> + + {/* Navigation buttons */} + <div className="flex gap-8 text-xs"> + <button + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + className="px-3 py-1 border border-blue-200 rounded font-variant-emoji-text hover:cursor-pointer hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" - > - {'⬅\uFE0E'} - </button> - <button - onClick={() => table.nextPage()} - disabled={!table.getCanNextPage()} - className="px-3 py-1 border border-blue-200 rounded + > + {"⬅\uFE0E"} + </button> + <button + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + className="px-3 py-1 border border-blue-200 rounded font-variant-emoji-text hover:cursor-pointer hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" - > - {'⮕\uFE0E'} - </button> - </div> - - {/* Page size selector */} - <select - value={table.getState().pagination.pageSize} - onChange={e => table.setPageSize(Number(e.target.value))} - className="px-2 py-1 border border-blue-200 rounded + > + {"⮕\uFE0E"} + </button> + </div> + + {/* Page size selector */} + <select + value={table.getState().pagination.pageSize} + onChange={(e) => table.setPageSize(Number(e.target.value))} + className="px-2 py-1 border border-blue-200 rounded hover:cursor-pointer hover:bg-blue-700" - > - {[10, 20, 30, 40].map(pageSize => ( - <option key={pageSize} value={pageSize}> - Show {pageSize} - </option> - ))} - </select> - </div> - </td> - </tr> - </tfoot> - - </table> - </div> - ) + > + {[10, 20, 30, 40].map((pageSize) => ( + <option key={pageSize} value={pageSize}> + Show {pageSize} + </option> + ))} + </select> + </div> + </td> + </tr> + </tfoot> + </table> + </div> + ); }; - const Wallet = () => { - const dispatch = useAppDispatch() - const bpuid = useAppSelector(state => state.app.bpuid) - const pagination = useAppSelector(state => state.app.txPagination); - - useEffect(() => { - if (!bpuid) return; - - new WalletApi().getUserTransactionHistoryProductIdTransactionHistoryGet( - bpid, // productId - bpuid, // bpuid - undefined, // createdAfter - undefined, // createdBefore - "-created", // orderBy - pagination.pageIndex + 1, // page - pagination.pageSize // pageSize - ).then(res => { - const response = res.data as UserLedgerTransactionsResponse; - dispatch(setUserLedgerSummary(response.summary)); - dispatch(setUserLedgerTxs(response.transactions ?? [])) - - dispatch(setTxPagination({ - pageIndex: (response.page ?? 1) - 1, - pageSize: response.size ?? 10, - })) - dispatch(setTxTotalItems(response.total ?? 0)) - dispatch(setTxTotalPages(response.pages ?? 9)) - }); - - }, [bpuid]) - - if (!bpuid) return null; - - return ( - <div className="fixed top-2 left-2 p-2 rounded + const dispatch = useAppDispatch(); + const pagination = useAppSelector((state) => state.app.txPagination); + const { data: user } = useGetMeQuery(); + + useEffect(() => { + if (!user?.product_user_id) return; + + new WalletApi() + .getUserTransactionHistoryProductIdTransactionHistoryGet( + bpid, // productId + user.product_user_id, + undefined, // createdAfter + undefined, // createdBefore + "-created", // orderBy + pagination.pageIndex + 1, // page + pagination.pageSize, // pageSize + ) + .then((res) => { + const response = res.data as UserLedgerTransactionsResponse; + dispatch(setUserLedgerSummary(response.summary)); + dispatch(setUserLedgerTxs(response.transactions ?? [])); + + dispatch( + setTxPagination({ + pageIndex: (response.page ?? 1) - 1, + pageSize: response.size ?? 10, + }), + ); + dispatch(setTxTotalItems(response.total ?? 0)); + dispatch(setTxTotalPages(response.pages ?? 9)); + }); + }, [user?.product_user_id]); + + if (!user?.product_user_id) return null; + + return ( + <div + className="fixed top-2 left-2 p-2 rounded bg-blue-500 text-white shadow-lg z-50 font-mono max-h-[calc(100vh-2.5rem)] overflow-y-auto max-w-[96vw] - "> - <WalletHeader /> - <WalletSummary /> - <WalletTransactionsHeader /> - </div> - ) + " + > + <WalletHeader /> + <WalletSummary /> + <WalletTransactionsHeader /> + </div> + ); }; - -export default Wallet;
\ No newline at end of file +export default Wallet; diff --git a/jb-ui/src/components/ui/button.tsx b/jb-ui/src/components/ui/button.tsx index b5ea4ab..d2176b4 100644 --- a/jb-ui/src/components/ui/button.tsx +++ b/jb-ui/src/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -35,8 +35,8 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); function Button({ className, @@ -46,9 +46,9 @@ function Button({ ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot.Root : "button" + const Comp = asChild ? Slot.Root : "button"; return ( <Comp @@ -58,7 +58,7 @@ function Button({ className={cn(buttonVariants({ variant, size, className }))} {...props} /> - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/jb-ui/src/components/ui/checkbox.tsx b/jb-ui/src/components/ui/checkbox.tsx index 0e2a6cd..f24ad45 100644 --- a/jb-ui/src/components/ui/checkbox.tsx +++ b/jb-ui/src/components/ui/checkbox.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import { CheckIcon } from "lucide-react" +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { CheckIcon } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Checkbox({ className, @@ -13,7 +13,7 @@ function Checkbox({ data-slot="checkbox" className={cn( "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", - className + className, )} {...props} > @@ -24,7 +24,7 @@ function Checkbox({ <CheckIcon className="size-3.5" /> </CheckboxPrimitive.Indicator> </CheckboxPrimitive.Root> - ) + ); } -export { Checkbox } +export { Checkbox }; diff --git a/jb-ui/src/components/ui/confetti.tsx b/jb-ui/src/components/ui/confetti.tsx index e0d353a..57d3273 100644 --- a/jb-ui/src/components/ui/confetti.tsx +++ b/jb-ui/src/components/ui/confetti.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react" +import type { ReactNode } from "react"; import React, { createContext, forwardRef, @@ -7,30 +7,30 @@ import React, { useImperativeHandle, useMemo, useRef, -} from "react" +} from "react"; import type { GlobalOptions as ConfettiGlobalOptions, CreateTypes as ConfettiInstance, Options as ConfettiOptions, -} from "canvas-confetti" -import confetti from "canvas-confetti" +} from "canvas-confetti"; +import confetti from "canvas-confetti"; -import { Button } from "@/components/ui/button" +import { Button } from "@/components/ui/button"; type Api = { - fire: (options?: ConfettiOptions) => void -} + fire: (options?: ConfettiOptions) => void; +}; type Props = React.ComponentPropsWithRef<"canvas"> & { - options?: ConfettiOptions - globalOptions?: ConfettiGlobalOptions - manualstart?: boolean - children?: ReactNode -} + options?: ConfettiOptions; + globalOptions?: ConfettiGlobalOptions; + manualstart?: boolean; + children?: ReactNode; +}; -export type ConfettiRef = Api | null +export type ConfettiRef = Api | null; -const ConfettiContext = createContext<Api>({} as Api) +const ConfettiContext = createContext<Api>({} as Api); // Define component first const ConfettiComponent = forwardRef<ConfettiRef, Props>((props, ref) => { @@ -40,76 +40,76 @@ const ConfettiComponent = forwardRef<ConfettiRef, Props>((props, ref) => { manualstart = false, children, ...rest - } = props - const instanceRef = useRef<ConfettiInstance | null>(null) + } = props; + const instanceRef = useRef<ConfettiInstance | null>(null); const canvasRef = useCallback( (node: HTMLCanvasElement) => { if (node !== null) { - if (instanceRef.current) return + if (instanceRef.current) return; instanceRef.current = confetti.create(node, { ...globalOptions, resize: true, - }) + }); } else { if (instanceRef.current) { - instanceRef.current.reset() - instanceRef.current = null + instanceRef.current.reset(); + instanceRef.current = null; } } }, - [globalOptions] - ) + [globalOptions], + ); const fire = useCallback( async (opts = {}) => { try { - await instanceRef.current?.({ ...options, ...opts }) + await instanceRef.current?.({ ...options, ...opts }); } catch (error) { - console.error("Confetti error:", error) + console.error("Confetti error:", error); } }, - [options] - ) + [options], + ); const api = useMemo( () => ({ fire, }), - [fire] - ) + [fire], + ); - useImperativeHandle(ref, () => api, [api]) + useImperativeHandle(ref, () => api, [api]); useEffect(() => { if (!manualstart) { - ;(async () => { + (async () => { try { - await fire() + await fire(); } catch (error) { - console.error("Confetti effect error:", error) + console.error("Confetti effect error:", error); } - })() + })(); } - }, [manualstart, fire]) + }, [manualstart, fire]); return ( <ConfettiContext.Provider value={api}> <canvas ref={canvasRef} {...rest} /> {children} </ConfettiContext.Provider> - ) -}) + ); +}); // Set display name immediately -ConfettiComponent.displayName = "Confetti" +ConfettiComponent.displayName = "Confetti"; // Export as Confetti -export const Confetti = ConfettiComponent +export const Confetti = ConfettiComponent; interface ConfettiButtonProps extends React.ComponentProps<"button"> { options?: ConfettiOptions & - ConfettiGlobalOptions & { canvas?: HTMLCanvasElement } + ConfettiGlobalOptions & { canvas?: HTMLCanvasElement }; } const ConfettiButtonComponent = ({ @@ -119,28 +119,28 @@ const ConfettiButtonComponent = ({ }: ConfettiButtonProps) => { const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => { try { - const rect = event.currentTarget.getBoundingClientRect() - const x = rect.left + rect.width / 2 - const y = rect.top + rect.height / 2 + const rect = event.currentTarget.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; await confetti({ ...options, origin: { x: x / window.innerWidth, y: y / window.innerHeight, }, - }) + }); } catch (error) { - console.error("Confetti button error:", error) + console.error("Confetti button error:", error); } - } + }; return ( <Button onClick={handleClick} {...props}> {children} </Button> - ) -} + ); +}; -ConfettiButtonComponent.displayName = "ConfettiButton" +ConfettiButtonComponent.displayName = "ConfettiButton"; -export const ConfettiButton = ConfettiButtonComponent +export const ConfettiButton = ConfettiButtonComponent; diff --git a/jb-ui/src/components/ui/dot-pattern.tsx b/jb-ui/src/components/ui/dot-pattern.tsx index 9522b7f..a217a9b 100644 --- a/jb-ui/src/components/ui/dot-pattern.tsx +++ b/jb-ui/src/components/ui/dot-pattern.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useId, useRef, useState } from "react" -import { motion } from "motion/react" +import React, { useEffect, useId, useRef, useState } from "react"; +import { motion } from "motion/react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; /** * DotPattern Component Props @@ -17,16 +17,16 @@ import { cn } from "@/lib/utils" * @param {boolean} [glow=false] - Whether dots should have a glowing animation effect */ interface DotPatternProps extends React.SVGProps<SVGSVGElement> { - width?: number - height?: number - x?: number - y?: number - cx?: number - cy?: number - cr?: number - className?: string - glow?: boolean - [key: string]: unknown + width?: number; + height?: number; + x?: number; + y?: number; + cx?: number; + cy?: number; + cr?: number; + className?: string; + glow?: boolean; + [key: string]: unknown; } /** @@ -71,22 +71,22 @@ export function DotPattern({ glow = false, ...props }: DotPatternProps) { - const id = useId() - const containerRef = useRef<SVGSVGElement>(null) - const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) + const id = useId(); + const containerRef = useRef<SVGSVGElement>(null); + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); useEffect(() => { const updateDimensions = () => { if (containerRef.current) { - const { width, height } = containerRef.current.getBoundingClientRect() - setDimensions({ width, height }) + const { width, height } = containerRef.current.getBoundingClientRect(); + setDimensions({ width, height }); } - } + }; - updateDimensions() - window.addEventListener("resize", updateDimensions) - return () => window.removeEventListener("resize", updateDimensions) - }, []) + updateDimensions(); + window.addEventListener("resize", updateDimensions); + return () => window.removeEventListener("resize", updateDimensions); + }, []); const dots = Array.from( { @@ -95,16 +95,16 @@ export function DotPattern({ Math.ceil(dimensions.height / height), }, (_, i) => { - const col = i % Math.ceil(dimensions.width / width) - const row = Math.floor(i / Math.ceil(dimensions.width / width)) + const col = i % Math.ceil(dimensions.width / width); + const row = Math.floor(i / Math.ceil(dimensions.width / width)); return { x: col * width + cx, y: row * height + cy, delay: Math.random() * 5, duration: Math.random() * 3 + 2, - } - } - ) + }; + }, + ); return ( <svg @@ -112,7 +112,7 @@ export function DotPattern({ aria-hidden="true" className={cn( "pointer-events-none absolute inset-0 h-full w-full text-neutral-400/80", - className + className, )} {...props} > @@ -152,5 +152,5 @@ export function DotPattern({ /> ))} </svg> - ) + ); } diff --git a/jb-ui/src/components/ui/field.tsx b/jb-ui/src/components/ui/field.tsx index db0dc12..77546e5 100644 --- a/jb-ui/src/components/ui/field.tsx +++ b/jb-ui/src/components/ui/field.tsx @@ -1,9 +1,9 @@ -import { useMemo } from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { useMemo } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" -import { Separator } from "@/components/ui/separator" +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { return ( @@ -12,11 +12,11 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { className={cn( "flex flex-col gap-6", "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", - className + className, )} {...props} /> - ) + ); } function FieldLegend({ @@ -32,11 +32,11 @@ function FieldLegend({ "mb-3 font-medium", "data-[variant=legend]:text-base", "data-[variant=label]:text-sm", - className + className, )} {...props} /> - ) + ); } function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { @@ -45,11 +45,11 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { data-slot="field-group" className={cn( "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4", - className + className, )} {...props} /> - ) + ); } const fieldVariants = cva( @@ -73,8 +73,8 @@ const fieldVariants = cva( defaultVariants: { orientation: "vertical", }, - } -) + }, +); function Field({ className, @@ -89,7 +89,7 @@ function Field({ className={cn(fieldVariants({ orientation }), className)} {...props} /> - ) + ); } function FieldContent({ className, ...props }: React.ComponentProps<"div">) { @@ -98,11 +98,11 @@ function FieldContent({ className, ...props }: React.ComponentProps<"div">) { data-slot="field-content" className={cn( "group/field-content flex flex-1 flex-col gap-1.5 leading-snug", - className + className, )} {...props} /> - ) + ); } function FieldLabel({ @@ -116,11 +116,11 @@ function FieldLabel({ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50", "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4", "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10", - className + className, )} {...props} /> - ) + ); } function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { @@ -129,11 +129,11 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { data-slot="field-label" className={cn( "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50", - className + className, )} {...props} /> - ) + ); } function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { @@ -144,11 +144,11 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { "text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance", "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5", "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", - className + className, )} {...props} /> - ) + ); } function FieldSeparator({ @@ -156,7 +156,7 @@ function FieldSeparator({ className, ...props }: React.ComponentProps<"div"> & { - children?: React.ReactNode + children?: React.ReactNode; }) { return ( <div @@ -164,7 +164,7 @@ function FieldSeparator({ data-content={!!children} className={cn( "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", - className + className, )} {...props} > @@ -178,7 +178,7 @@ function FieldSeparator({ </span> )} </div> - ) + ); } function FieldError({ @@ -187,37 +187,37 @@ function FieldError({ errors, ...props }: React.ComponentProps<"div"> & { - errors?: Array<{ message?: string } | undefined> + errors?: Array<{ message?: string } | undefined>; }) { const content = useMemo(() => { if (children) { - return children + return children; } if (!errors?.length) { - return null + return null; } const uniqueErrors = [ ...new Map(errors.map((error) => [error?.message, error])).values(), - ] + ]; if (uniqueErrors?.length == 1) { - return uniqueErrors[0]?.message + return uniqueErrors[0]?.message; } return ( <ul className="ml-4 flex list-disc flex-col gap-1"> {uniqueErrors.map( (error, index) => - error?.message && <li key={index}>{error.message}</li> + error?.message && <li key={index}>{error.message}</li>, )} </ul> - ) - }, [children, errors]) + ); + }, [children, errors]); if (!content) { - return null + return null; } return ( @@ -229,7 +229,7 @@ function FieldError({ > {content} </div> - ) + ); } export { @@ -243,4 +243,4 @@ export { FieldSet, FieldContent, FieldTitle, -} +}; diff --git a/jb-ui/src/components/ui/label.tsx b/jb-ui/src/components/ui/label.tsx index ef7133a..4ef28a9 100644 --- a/jb-ui/src/components/ui/label.tsx +++ b/jb-ui/src/components/ui/label.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Label({ className, @@ -12,11 +12,11 @@ function Label({ data-slot="label" className={cn( "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", - className + className, )} {...props} /> - ) + ); } -export { Label } +export { Label }; diff --git a/jb-ui/src/components/ui/magic-card.tsx b/jb-ui/src/components/ui/magic-card.tsx index e6503d8..2b92e38 100644 --- a/jb-ui/src/components/ui/magic-card.tsx +++ b/jb-ui/src/components/ui/magic-card.tsx @@ -1,16 +1,16 @@ -import React, { useCallback, useEffect } from "react" -import { motion, useMotionTemplate, useMotionValue } from "motion/react" +import React, { useCallback, useEffect } from "react"; +import { motion, useMotionTemplate, useMotionValue } from "motion/react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; interface MagicCardProps { - children?: React.ReactNode - className?: string - gradientSize?: number - gradientColor?: string - gradientOpacity?: number - gradientFrom?: string - gradientTo?: string + children?: React.ReactNode; + className?: string; + gradientSize?: number; + gradientColor?: string; + gradientOpacity?: number; + gradientFrom?: string; + gradientTo?: string; } export function MagicCard({ @@ -22,49 +22,49 @@ export function MagicCard({ gradientFrom = "#9E7AFF", gradientTo = "#FE8BBB", }: MagicCardProps) { - const mouseX = useMotionValue(-gradientSize) - const mouseY = useMotionValue(-gradientSize) + const mouseX = useMotionValue(-gradientSize); + const mouseY = useMotionValue(-gradientSize); const reset = useCallback(() => { - mouseX.set(-gradientSize) - mouseY.set(-gradientSize) - }, [gradientSize, mouseX, mouseY]) + mouseX.set(-gradientSize); + mouseY.set(-gradientSize); + }, [gradientSize, mouseX, mouseY]); const handlePointerMove = useCallback( (e: React.PointerEvent<HTMLDivElement>) => { - const rect = e.currentTarget.getBoundingClientRect() - mouseX.set(e.clientX - rect.left) - mouseY.set(e.clientY - rect.top) + const rect = e.currentTarget.getBoundingClientRect(); + mouseX.set(e.clientX - rect.left); + mouseY.set(e.clientY - rect.top); }, - [mouseX, mouseY] - ) + [mouseX, mouseY], + ); useEffect(() => { - reset() - }, [reset]) + reset(); + }, [reset]); useEffect(() => { const handleGlobalPointerOut = (e: PointerEvent) => { if (!e.relatedTarget) { - reset() + reset(); } - } + }; const handleVisibility = () => { if (document.visibilityState !== "visible") { - reset() + reset(); } - } + }; - window.addEventListener("pointerout", handleGlobalPointerOut) - window.addEventListener("blur", reset) - document.addEventListener("visibilitychange", handleVisibility) + window.addEventListener("pointerout", handleGlobalPointerOut); + window.addEventListener("blur", reset); + document.addEventListener("visibilitychange", handleVisibility); return () => { - window.removeEventListener("pointerout", handleGlobalPointerOut) - window.removeEventListener("blur", reset) - document.removeEventListener("visibilitychange", handleVisibility) - } - }, [reset]) + window.removeEventListener("pointerout", handleGlobalPointerOut); + window.removeEventListener("blur", reset); + document.removeEventListener("visibilitychange", handleVisibility); + }; + }, [reset]); return ( <div @@ -97,5 +97,5 @@ export function MagicCard({ /> <div className="relative">{children}</div> </div> - ) + ); } diff --git a/jb-ui/src/components/ui/pagination.tsx b/jb-ui/src/components/ui/pagination.tsx index 1dcfb0c..990f907 100644 --- a/jb-ui/src/components/ui/pagination.tsx +++ b/jb-ui/src/components/ui/pagination.tsx @@ -1,12 +1,12 @@ -import * as React from "react" +import * as React from "react"; import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon, -} from "lucide-react" +} from "lucide-react"; -import { cn } from "@/lib/utils" -import { buttonVariants, type Button } from "@/components/ui/button" +import { cn } from "@/lib/utils"; +import { buttonVariants, type Button } from "@/components/ui/button"; function Pagination({ className, ...props }: React.ComponentProps<"nav">) { return ( @@ -17,7 +17,7 @@ function Pagination({ className, ...props }: React.ComponentProps<"nav">) { className={cn("mx-auto flex w-full justify-center", className)} {...props} /> - ) + ); } function PaginationContent({ @@ -30,17 +30,17 @@ function PaginationContent({ className={cn("flex flex-row items-center gap-1", className)} {...props} /> - ) + ); } function PaginationItem({ ...props }: React.ComponentProps<"li">) { - return <li data-slot="pagination-item" {...props} /> + return <li data-slot="pagination-item" {...props} />; } type PaginationLinkProps = { - isActive?: boolean + isActive?: boolean; } & Pick<React.ComponentProps<typeof Button>, "size"> & - React.ComponentProps<"a"> + React.ComponentProps<"a">; function PaginationLink({ className, @@ -58,11 +58,11 @@ function PaginationLink({ variant: isActive ? "outline" : "ghost", size, }), - className + className, )} {...props} /> - ) + ); } function PaginationPrevious({ @@ -79,7 +79,7 @@ function PaginationPrevious({ <ChevronLeftIcon /> <span className="hidden sm:block">Previous</span> </PaginationLink> - ) + ); } function PaginationNext({ @@ -96,7 +96,7 @@ function PaginationNext({ <span className="hidden sm:block">Next</span> <ChevronRightIcon /> </PaginationLink> - ) + ); } function PaginationEllipsis({ @@ -113,7 +113,7 @@ function PaginationEllipsis({ <MoreHorizontalIcon className="size-4" /> <span className="sr-only">More pages</span> </span> - ) + ); } export { @@ -124,4 +124,4 @@ export { PaginationPrevious, PaginationNext, PaginationEllipsis, -} +}; diff --git a/jb-ui/src/components/ui/separator.tsx b/jb-ui/src/components/ui/separator.tsx index 275381c..72c18e3 100644 --- a/jb-ui/src/components/ui/separator.tsx +++ b/jb-ui/src/components/ui/separator.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Separator({ className, @@ -18,11 +18,11 @@ function Separator({ orientation={orientation} className={cn( "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", - className + className, )} {...props} /> - ) + ); } -export { Separator } +export { Separator }; diff --git a/jb-ui/src/components/ui/skeleton.tsx b/jb-ui/src/components/ui/skeleton.tsx index 32ea0ef..0168998 100644 --- a/jb-ui/src/components/ui/skeleton.tsx +++ b/jb-ui/src/components/ui/skeleton.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Skeleton({ className, ...props }: React.ComponentProps<"div">) { return ( @@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) { className={cn("bg-accent animate-pulse rounded-md", className)} {...props} /> - ) + ); } -export { Skeleton } +export { Skeleton }; diff --git a/jb-ui/src/hooks.ts b/jb-ui/src/hooks.ts index 02ff1b6..7f8db4b 100644 --- a/jb-ui/src/hooks.ts +++ b/jb-ui/src/hooks.ts @@ -1,11 +1,11 @@ // This file serves as a central hub for re-exporting pre-typed Redux hooks. -import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux' -import type { AppDispatch, RootState } from '@/store' - +import { useDispatch, useSelector, TypedUseSelectorHook } from "react-redux"; +import type { AppDispatch, RootState } from "@/store"; // Use throughout your app instead of plain `useDispatch` and `useSelector` // export const useAppDispatch: () => AppDispatch = useDispatch.withTypes<AppDispatch>() -export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector.withTypes<RootState>() +export const useAppSelector: TypedUseSelectorHook<RootState> = + useSelector.withTypes<RootState>(); -export const useAppDispatch: () => AppDispatch = useDispatch +export const useAppDispatch: () => AppDispatch = useDispatch; // export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector diff --git a/jb-ui/src/index.css b/jb-ui/src/index.css index 1677b7b..579102b 100644 --- a/jb-ui/src/index.css +++ b/jb-ui/src/index.css @@ -138,4 +138,3 @@ @apply bg-background text-foreground; } } - diff --git a/jb-ui/src/lib/utils.ts b/jb-ui/src/lib/utils.ts index 0c84192..1aa59c6 100644 --- a/jb-ui/src/lib/utils.ts +++ b/jb-ui/src/lib/utils.ts @@ -1,113 +1,119 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" -import { Source } from "../api_fsb" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; +import { Source } from "../api_fsb"; export const routeBasename = import.meta.env.VITE_BASENAME; -export const tagManager = import.meta.env.VITE_TAG_MANAGER +export const tagManager = import.meta.env.VITE_TAG_MANAGER; export const bpid = import.meta.env.VITE_BPID; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } export function assert(condition: any, msg?: string): asserts condition { - if (!condition) { - throw new Error(msg); - } + if (!condition) { + throw new Error(msg); + } } export function toCapitalized(word: string): string { - return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); } -export function truncate(originalText: string, maxLength: number = 10, replaceStr = "…"): string { - if (originalText && originalText.length > maxLength) { - return originalText.substring(0, maxLength) + replaceStr; - } - return originalText; +export function truncate( + originalText: string, + maxLength: number = 10, + replaceStr = "…", +): string { + if (originalText && originalText.length > maxLength) { + return originalText.substring(0, maxLength) + replaceStr; + } + return originalText; } export function usdCentFmt(value: number): string { - return (value / 100).toLocaleString('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }) + return (value / 100).toLocaleString("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); } export function formatStatus(status: string): string { - const table = { - 'c': 'Completed', - 't': 'Terminated', - 'f': 'Failed', - 'e': 'Entered', - } - - return table[status as keyof typeof table] ?? "Unknown"; + const table = { + c: "Completed", + t: "Terminated", + f: "Failed", + e: "Entered", + }; + + return table[status as keyof typeof table] ?? "Unknown"; } export function formatSource(source: Source): string { - const table = { - 'g': 'GRS', - 'c': 'Cint', - 'a': 'Dalia', - 'd': 'Dynata', - 'et': 'Etx', - 'f': 'Full Circle', - 'i': 'InnovateMR', - 'l': 'Lucid', - 'm': 'Morning Consult', - 'n': 'Open Labs', - 'o': 'Pollfish', - 'e': 'Precision Sample', - 'r': 'Prodege User', - 'pr': 'Prodege', - 'p': 'Pulley', - 'rd': 'Rep Data', - 'h': 'Sago', - 's': 'Pure Spectrum', - 't': 'Testing', - 'u': 'Testing 2', - 'w': 'WXET' - } - - return table[source] + const table = { + g: "GRS", + c: "Cint", + a: "Dalia", + d: "Dynata", + et: "Etx", + f: "Full Circle", + i: "InnovateMR", + l: "Lucid", + m: "Morning Consult", + n: "Open Labs", + o: "Pollfish", + e: "Precision Sample", + r: "Prodege User", + pr: "Prodege", + p: "Pulley", + rd: "Rep Data", + h: "Sago", + s: "Pure Spectrum", + t: "Testing", + u: "Testing 2", + w: "WXET", + }; + + return table[source]; } export function formatSecondsVerbose(seconds: number): string { - const mins = Math.floor(seconds / 60) - const secs = seconds % 60 - - const parts = [] - if (mins > 0) parts.push(`${mins} min`) - if (secs > 0 || mins === 0) { - parts.push(`${secs.toLocaleString('en-US', { - minimumFractionDigits: 0, - maximumFractionDigits: 1 - })} sec`) - } - - return parts.join(" ") + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + + const parts = []; + if (mins > 0) parts.push(`${mins} min`); + if (secs > 0 || mins === 0) { + parts.push( + `${secs.toLocaleString("en-US", { + minimumFractionDigits: 0, + maximumFractionDigits: 1, + })} sec`, + ); + } + + return parts.join(" "); } export function formatSeconds(seconds: number): string { - const mins = Math.floor(seconds / 60) - const secs = seconds % 60 - const paddedSecs = secs.toString().padStart(2, '0') - return `${mins}:${paddedSecs}` + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + const paddedSecs = secs.toString().padStart(2, "0"); + return `${mins}:${paddedSecs}`; } export function formatCentsToUSD(cents: number): string { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - }).format(cents / 100) + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(cents / 100); } export function titleCase(str: string): string { - return str - .split(" ") - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); -}
\ No newline at end of file + return str + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} diff --git a/jb-ui/src/main.tsx b/jb-ui/src/main.tsx index 538bea2..6e02727 100644 --- a/jb-ui/src/main.tsx +++ b/jb-ui/src/main.tsx @@ -1,19 +1,19 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { Provider } from 'react-redux' -import { store } from '@/store' +import React from "react"; +import ReactDOM from "react-dom/client"; +import { Provider } from "react-redux"; +import { store } from "@/store"; import JBApp from "@/JBApp"; -const rootElement = document.getElementById('root'); +const rootElement = document.getElementById("root"); if (!rootElement) { - throw new Error('Failed to find the root element'); + throw new Error("Failed to find the root element"); } const root = ReactDOM.createRoot(rootElement); root.render( - <React.StrictMode> - <Provider store={store}> - <JBApp /> - </Provider> - </React.StrictMode > -);
\ No newline at end of file + <React.StrictMode> + <Provider store={store}> + <JBApp /> + </Provider> + </React.StrictMode>, +); diff --git a/jb-ui/src/models/app.ts b/jb-ui/src/models/app.ts index 95b4c11..db54e0d 100644 --- a/jb-ui/src/models/app.ts +++ b/jb-ui/src/models/app.ts @@ -1,37 +1,47 @@ import { - UserWalletBalance, TopNPlusBucket, TaskStatusResponse, - UserLedgerTransactionTypesSummary, - UserLedgerTransactionsResponseTransactionsInner, - OfferwallReason + UserWalletBalance, + TopNPlusBucket, + TaskStatusResponse, + UserLedgerTransactionTypesSummary, + UserLedgerTransactionsResponseTransactionsInner, + OfferwallReason, + UserLedgerWallet, + UserDisplayedWalletBalance, } from "@/api_fsb"; -import { PaginationState } from '@tanstack/react-table'; +import { PaginationState } from "@tanstack/react-table"; export interface App { - // Global IDs needed by GRL + MTurk - bpuid?: string; - assignment_id?: string; - turkSubmitTo?: string; + // Global IDs needed by GRL + MTurk + bpuid?: string; + assignment_id?: string; + turkSubmitTo?: string; - // Global UI settings - loi: number; - availability_count?: number; - attempted_live_eligible_count?: number; - offerwall_reasons: OfferwallReason[]; + // Global UI settings + loi: number; + availability_count?: number; + attempted_live_eligible_count?: number; + offerwall_reasons: OfferwallReason[]; - // The timestamp of when the currently requested bucket was requested. We want - // to save this to prevent it from sitting for longer than 120 seconds. - currentBucketRequested?: number; // Date.now() is a timestamp in ms - currentBuckets?: TopNPlusBucket[]; - currentBucketEntered?: number; // Date.now() is a timestamp in ms - taskStatus?: TaskStatusResponse; + // The timestamp of when the currently requested bucket was requested. We want + // to save this to prevent it from sitting for longer than 120 seconds. + currentBucketRequested?: number; // Date.now() is a timestamp in ms + currentBuckets?: TopNPlusBucket[]; + currentBucketEntered?: number; // Date.now() is a timestamp in ms + taskStatus?: TaskStatusResponse; - // Wallet stuff - userWalletBalance?: UserWalletBalance; - userLedgerSummary?: UserLedgerTransactionTypesSummary; - userLedgerTxCount?: number; - userLedgerTxs: UserLedgerTransactionsResponseTransactionsInner[]; + // Wallet stuff + userWalletBalance?: UserWalletBalance; + userLedgerSummary?: UserLedgerTransactionTypesSummary; + userLedgerTxCount?: number; + userLedgerTxs: UserLedgerTransactionsResponseTransactionsInner[]; - txPagination: PaginationState; - txTotalItems?: number; - txTotalPages?: number; + // --- new approach + userDisplayedWalletBalance?: UserDisplayedWalletBalance; + userWallet?: UserLedgerWallet; + userAttemptCreditWallet?: UserLedgerWallet; + // --- + + txPagination: PaginationState; + txTotalItems?: number; + txTotalPages?: number; } diff --git a/jb-ui/src/models/appSlice.ts b/jb-ui/src/models/appSlice.ts index 87952d9..42a776e 100644 --- a/jb-ui/src/models/appSlice.ts +++ b/jb-ui/src/models/appSlice.ts @@ -1,188 +1,221 @@ import { - TaskStatusResponse, - TopNPlusBucket, - UserLedgerTransactionsResponseTransactionsInner, - UserLedgerTransactionTypesSummary, - UserWalletBalance, - OfferwallReason + TaskStatusResponse, + TopNPlusBucket, + UserLedgerTransactionsResponseTransactionsInner, + UserLedgerTransactionTypesSummary, + UserWalletBalance, + UserDisplayedWalletBalance, + UserLedgerWallet, + OfferwallReason, } from "@/api_fsb"; import { App } from "@/models/app"; import { RootState } from "@/store"; -import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { PaginationState } from '@tanstack/react-table'; +import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { PaginationState } from "@tanstack/react-table"; import moment from "moment"; const initialState: App = { - bpuid: undefined, - assignment_id: undefined, - loi: 1800, - currentBuckets: undefined, - currentBucketEntered: undefined, - taskStatus: undefined, - offerwall_reasons: [], - - userWalletBalance: undefined, - userLedgerSummary: undefined, - userLedgerTxCount: undefined, - userLedgerTxs: [], - - txPagination: { pageIndex: 0, pageSize: 10 }, - txTotalItems: undefined, - txTotalPages: undefined -} as App + bpuid: undefined, + assignment_id: undefined, + loi: 1800, + currentBuckets: undefined, + currentBucketEntered: undefined, + taskStatus: undefined, + offerwall_reasons: [], + + userWalletBalance: undefined, + userLedgerSummary: undefined, + userLedgerTxCount: undefined, + userLedgerTxs: [], + + // --- new approach + userDisplayedWalletBalance: undefined, + userWallet: undefined, + userAttemptCreditWallet: undefined, + // --- + + txPagination: { pageIndex: 0, pageSize: 10 }, + txTotalItems: undefined, + txTotalPages: undefined, +} as App; const appSlice = createSlice({ - name: 'app', - initialState, - reducers: { - - setProductUserID(state, action: PayloadAction<string>) { - state.bpuid = action.payload; - }, - - setAssignmentID(state, action: PayloadAction<string>) { - // This is really so silly. Amazon should simply not send - // anything if it's unavailable. - if (action.payload === "ASSIGNMENT_ID_NOT_AVAILABLE") { - state.assignment_id = undefined; - } - state.assignment_id = action.payload; - }, - - setTurkSubmitTo(state, action: PayloadAction<string>) { - state.turkSubmitTo = action.payload; - }, - - setLOI(state, action: PayloadAction<number>) { - state.loi = action.payload; - }, - - setAvailabilityCount(state, action: PayloadAction<number>) { - state.availability_count = action.payload - }, - - setAttemptedLiveEligibleCount(state, action: PayloadAction<number>) { - state.attempted_live_eligible_count = action.payload - }, - - setCurrentBuckets(state, action: PayloadAction<TopNPlusBucket[] | undefined>) { - state.currentBucketRequested = moment.utc().unix(); - state.currentBuckets = action.payload - }, - - setOfferwallReasons(state, action: PayloadAction<OfferwallReason[]>) { - state.offerwall_reasons = action.payload; - }, - - setEnteredTimestamp(state) { - // Go back by 2 seconds to account for any time drift - state.currentBucketEntered = moment.utc().unix() - 2; - }, - - setTaskStatus(state, action: PayloadAction<TaskStatusResponse>) { - state.taskStatus = action.payload; - - state.bpuid = action.payload.product_user_id; - }, - - setUserWalletBalance(state, action: PayloadAction<UserWalletBalance>) { - state.userWalletBalance = action.payload; - }, - - setUserLedgerSummary(state, action: PayloadAction<UserLedgerTransactionTypesSummary>) { - state.userLedgerSummary = action.payload; - }, - - setUserLedgerTxs(state, action: PayloadAction<UserLedgerTransactionsResponseTransactionsInner[]>) { - // We're not appending the transaction details, it's only going - // to reassign the current page of Transactions. - state.userLedgerTxs = action.payload; - }, - - setTxPagination(state, action: PayloadAction<PaginationState>) { - state.txPagination = action.payload; - }, - - setTxTotalItems(state, action: PayloadAction<number>) { - state.txTotalItems = action.payload; - }, - - setTxTotalPages(state, action: PayloadAction<number>) { - state.txTotalPages = action.payload; - } - - } -}) + name: "app", + initialState, + reducers: { + setProductUserID(state, action: PayloadAction<string>) { + state.bpuid = action.payload; + }, + + setAssignmentID(state, action: PayloadAction<string>) { + // This is really so silly. Amazon should simply not send + // anything if it's unavailable. + if (action.payload === "ASSIGNMENT_ID_NOT_AVAILABLE") { + state.assignment_id = undefined; + } + state.assignment_id = action.payload; + }, + + setTurkSubmitTo(state, action: PayloadAction<string>) { + state.turkSubmitTo = action.payload; + }, + + setLOI(state, action: PayloadAction<number>) { + state.loi = action.payload; + }, + + setAvailabilityCount(state, action: PayloadAction<number>) { + state.availability_count = action.payload; + }, + + setAttemptedLiveEligibleCount(state, action: PayloadAction<number>) { + state.attempted_live_eligible_count = action.payload; + }, + + setCurrentBuckets( + state, + action: PayloadAction<TopNPlusBucket[] | undefined>, + ) { + state.currentBucketRequested = moment.utc().unix(); + state.currentBuckets = action.payload; + }, + + setOfferwallReasons(state, action: PayloadAction<OfferwallReason[]>) { + state.offerwall_reasons = action.payload; + }, + + setEnteredTimestamp(state) { + // Go back by 2 seconds to account for any time drift + state.currentBucketEntered = moment.utc().unix() - 2; + }, + + setTaskStatus(state, action: PayloadAction<TaskStatusResponse>) { + state.taskStatus = action.payload; + + state.bpuid = action.payload.product_user_id; + }, + + setUserWalletBalance(state, action: PayloadAction<UserWalletBalance>) { + state.userWalletBalance = action.payload; + }, + + // --- New approach --- + setUserDisplayedWalletBalance( + state, + action: PayloadAction<UserDisplayedWalletBalance>, + ) { + state.userDisplayedWalletBalance = action.payload; + }, + + setUserWallet(state, action: PayloadAction<UserLedgerWallet>) { + state.userWallet = action.payload; + }, + + setUserAttemptCreditWallet(state, action: PayloadAction<UserLedgerWallet>) { + state.userAttemptCreditWallet = action.payload; + }, + + // --- --- + + setUserLedgerSummary( + state, + action: PayloadAction<UserLedgerTransactionTypesSummary>, + ) { + state.userLedgerSummary = action.payload; + }, + + setUserLedgerTxs( + state, + action: PayloadAction<UserLedgerTransactionsResponseTransactionsInner[]>, + ) { + // We're not appending the transaction details, it's only going + // to reassign the current page of Transactions. + state.userLedgerTxs = action.payload; + }, + + setTxPagination(state, action: PayloadAction<PaginationState>) { + state.txPagination = action.payload; + }, + + setTxTotalItems(state, action: PayloadAction<number>) { + state.txTotalItems = action.payload; + }, + + setTxTotalPages(state, action: PayloadAction<number>) { + state.txTotalPages = action.payload; + }, + }, +}); export const { - setProductUserID, - setAssignmentID, - setTurkSubmitTo, - setLOI, - setAvailabilityCount, - setAttemptedLiveEligibleCount, - setOfferwallReasons, - setCurrentBuckets, - setEnteredTimestamp, - setTaskStatus, - - setUserWalletBalance, - - setUserLedgerSummary, - setUserLedgerTxs, - - setTxPagination, - setTxTotalItems, - setTxTotalPages + setProductUserID, + setAssignmentID, + setTurkSubmitTo, + setLOI, + setAvailabilityCount, + setAttemptedLiveEligibleCount, + setOfferwallReasons, + setCurrentBuckets, + setEnteredTimestamp, + setTaskStatus, + + setUserWalletBalance, + + setUserLedgerSummary, + setUserLedgerTxs, + + setUserDisplayedWalletBalance, + setUserWallet, + setUserAttemptCreditWallet, + + setTxPagination, + setTxTotalItems, + setTxTotalPages, } = appSlice.actions; -export default appSlice.reducer +export default appSlice.reducer; export const getLOIText = createSelector( - [(state: RootState) => state.app], - (app_config): string => { - - const lookup: Record<number, string> = { - 600: "ten", - 1200: "twenty", - 1800: "thirty", - }; - - return lookup[app_config.loi] || " – "; - } + [(state: RootState) => state.app], + (app_config): string => { + const lookup: Record<number, string> = { + 600: "ten", + 1200: "twenty", + 1800: "thirty", + }; + + return lookup[app_config.loi] || " – "; + }, ); export const getAvailabilityCount = createSelector( - [(state: RootState) => state.app], - (app_config): number | undefined => { - return app_config.availability_count; - } + [(state: RootState) => state.app], + (app_config): number | undefined => { + return app_config.availability_count; + }, ); export const isLowBalance = createSelector( - [(state: RootState) => state.app], - (app_config): boolean => { - const bal_amt = app_config.userWalletBalance?.amount ?? 0 - return bal_amt <= -90; - } + [(state: RootState) => state.app], + (app_config): boolean => { + const bal_amt = app_config.userWalletBalance?.amount ?? 0; + return bal_amt <= -90; + }, ); - export const selectBucket = createSelector( - [ - (state: RootState) => state.app.currentBuckets, - ], - (buckets): TopNPlusBucket | null => { - if (buckets && buckets.length >= 1) { - return buckets[0] - } - return null; + [(state: RootState) => state.app.currentBuckets], + (buckets): TopNPlusBucket | null => { + if (buckets && buckets.length >= 1) { + return buckets[0]; } + return null; + }, ); export const getSurveyURL = createSelector( - [selectBucket], - (bucket): string | null => { - return bucket?.uri || null; - } -);
\ No newline at end of file + [selectBucket], + (bucket): string | null => { + return bucket?.uri || null; + }, +); diff --git a/jb-ui/src/models/authAPI.ts b/jb-ui/src/models/authAPI.ts new file mode 100644 index 0000000..fa08bc7 --- /dev/null +++ b/jb-ui/src/models/authAPI.ts @@ -0,0 +1,24 @@ +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import type { User } from "@/types/authApi"; + +export const authApi = createApi({ + reducerPath: "authApi", + baseQuery: fetchBaseQuery({ + baseUrl: "/auth", + credentials: "include", // sends the httpOnly cookie + }), + tagTypes: ["User"], + endpoints: (builder) => ({ + getMe: builder.query<User, void>({ + query: () => "/session", + providesTags: ["User"], + }), + // logout: builder.mutation({ + // query: () => ({ url: "/de-session", method: "POST" }), + // invalidatesTags: ["User"], + // }), + }), +}); + +// export const { useGetMeQuery, useLogoutMutation } = authApi; +export const { useGetMeQuery } = authApi; diff --git a/jb-ui/src/models/cashoutMethodsSlice.ts b/jb-ui/src/models/cashoutMethodsSlice.ts new file mode 100644 index 0000000..92c03da --- /dev/null +++ b/jb-ui/src/models/cashoutMethodsSlice.ts @@ -0,0 +1,37 @@ +import { CashoutMethodOut } from "@/api_fsb"; +import type { RootState } from "@/store"; +import { createSlice, createEntityAdapter } from "@reduxjs/toolkit"; + +// Entity adapter for normalized storage +const cashoutMethodsAdapter = createEntityAdapter({ + selectId: (model: CashoutMethodOut) => model.id!, + sortComparer: (a, b) => { + return (b.min_value ?? 0) - (a.min_value ?? 0); + }, +}); + +const cashoutMethodsSlice = createSlice({ + name: "cashoutMethods", + initialState: cashoutMethodsAdapter.getInitialState(), + + reducers: { + addCashoutMethod: cashoutMethodsAdapter.addOne, + addCashoutMethods: cashoutMethodsAdapter.addMany, + upsertCashoutMethod: cashoutMethodsAdapter.upsertOne, // Add or Update + }, +}); + +export const { addCashoutMethod, addCashoutMethods, upsertCashoutMethod } = + cashoutMethodsSlice.actions; + +export const cashoutMethodsSelectors = cashoutMethodsAdapter.getSelectors( + (state: RootState) => state.cashoutMethods, +); + +export const selectAllCashoutMethods = ( + state: RootState, +): CashoutMethodOut[] => { + return cashoutMethodsSelectors.selectAll(state) ?? []; +}; + +export default cashoutMethodsSlice.reducer; diff --git a/jb-ui/src/models/grlEventsSlice.ts b/jb-ui/src/models/grlEventsSlice.ts index 912c4ca..681c22e 100644 --- a/jb-ui/src/models/grlEventsSlice.ts +++ b/jb-ui/src/models/grlEventsSlice.ts @@ -1,58 +1,56 @@ import { EventEnvelope } from "@/api_fsb"; import { RootState } from "@/store"; -import { createEntityAdapter, createSelector, createSlice } from '@reduxjs/toolkit'; +import { + createEntityAdapter, + createSelector, + createSlice, +} from "@reduxjs/toolkit"; const eventAdapter = createEntityAdapter({ - selectId: (model: EventEnvelope) => model.event_uuid!, - sortComparer: (a, b) => { - // ASC by timestamp - return a.timestamp!.localeCompare(b.timestamp!); - }, + selectId: (model: EventEnvelope) => model.event_uuid!, + sortComparer: (a, b) => { + // ASC by timestamp + return a.timestamp!.localeCompare(b.timestamp!); + }, }); - const grlEventsSlice = createSlice({ - name: 'grlEvents', - initialState: eventAdapter.getInitialState(), - - reducers: { - addEvent: eventAdapter.addOne, - addEvents: eventAdapter.addMany, - upsertEvent: eventAdapter.upsertOne, // Add or Update - - } -}) - - -export const { - addEvent, addEvents, upsertEvent -} = grlEventsSlice.actions; + name: "grlEvents", + initialState: eventAdapter.getInitialState(), + + reducers: { + addEvent: eventAdapter.addOne, + addEvents: eventAdapter.addMany, + upsertEvent: eventAdapter.upsertOne, // Add or Update + }, +}); +export const { addEvent, addEvents, upsertEvent } = grlEventsSlice.actions; export const eventSelectors = eventAdapter.getSelectors( - (state: RootState) => state.events + (state: RootState) => state.events, ); -export const selectAllEvents = (state: RootState): EventEnvelope[] => eventSelectors.selectAll(state); - -const BASE_SPEED = 40; // px/sec at idle -const MAX_SPEED = 380; // px/sec at surge +export const selectAllEvents = (state: RootState): EventEnvelope[] => + eventSelectors.selectAll(state); -const RATE_WINDOW_MS = 10_000; // ms window to measure message frequency -const RATE_TO_SPEED = 55; // px/sec added per msg/sec +const BASE_SPEED = 40; // px/sec at idle +const MAX_SPEED = 380; // px/sec at surge +const RATE_WINDOW_MS = 10_000; // ms window to measure message frequency +const RATE_TO_SPEED = 55; // px/sec added per msg/sec -// --- Speed: derived from arrival rate of recent items --- +// --- Speed: derived from arrival rate of recent items --- export const selectCurrentSpeed = createSelector(selectAllEvents, (items) => { - const now = Date.now(); + const now = Date.now(); - const recentCount = items.filter((m) => { - const ts = new Date(m.timestamp!).getTime(); - return now - ts < RATE_WINDOW_MS; - }).length; + const recentCount = items.filter((m) => { + const ts = new Date(m.timestamp!).getTime(); + return now - ts < RATE_WINDOW_MS; + }).length; - const rate = recentCount / (RATE_WINDOW_MS / 1_000); // msgs/sec - return Math.min(BASE_SPEED + rate * RATE_TO_SPEED, MAX_SPEED); + const rate = recentCount / (RATE_WINDOW_MS / 1_000); // msgs/sec + return Math.min(BASE_SPEED + rate * RATE_TO_SPEED, MAX_SPEED); }); -export default grlEventsSlice.reducer +export default grlEventsSlice.reducer; diff --git a/jb-ui/src/models/grlStatsSlice.ts b/jb-ui/src/models/grlStatsSlice.ts index 9385256..004d321 100644 --- a/jb-ui/src/models/grlStatsSlice.ts +++ b/jb-ui/src/models/grlStatsSlice.ts @@ -1,54 +1,47 @@ import { StatsSnapshot } from "@/api_fsb"; import { RootState } from "@/store"; -import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit"; - -const initialState: StatsSnapshot[] = [] +const initialState: StatsSnapshot[] = []; const grlStatsSlice = createSlice({ - name: 'grlStats', - initialState, - reducers: { - - addStatsData(state, action: PayloadAction<StatsSnapshot>) { - state.push(action.payload); - } - } -}) - -export const { - addStatsData -} = grlStatsSlice.actions; -export default grlStatsSlice.reducer - + name: "grlStats", + initialState, + reducers: { + addStatsData(state, action: PayloadAction<StatsSnapshot>) { + state.push(action.payload); + }, + }, +}); + +export const { addStatsData } = grlStatsSlice.actions; +export default grlStatsSlice.reducer; export const selectRecentStats = createSelector( - [ - (state: RootState) => state.stats, - ], - (stats): StatsSnapshot | null => { - const lastStat = stats[stats.length - 1]; - return lastStat ?? null; - } + [(state: RootState) => state.stats], + (stats): StatsSnapshot | null => { + const lastStat = stats[stats.length - 1]; + return lastStat ?? null; + }, ); export const activeUsers = createSelector( - [selectRecentStats], - (recentStats): number | null => { - return recentStats?.active_users_last_24h ?? null; - } + [selectRecentStats], + (recentStats): number | null => { + return recentStats?.active_users_last_24h ?? null; + }, ); export const activeSurveys = createSelector( - [selectRecentStats], - (recentStats): number | null => { - return recentStats?.live_task_count?.total ?? null; - } + [selectRecentStats], + (recentStats): number | null => { + return recentStats?.live_task_count?.total ?? null; + }, ); export const maxPayout = createSelector( - [selectRecentStats], - (recentStats): number | null => { - return recentStats?.live_tasks_max_payout?.value ?? null; - } + [selectRecentStats], + (recentStats): number | null => { + return recentStats?.live_tasks_max_payout?.value ?? null; + }, ); diff --git a/jb-ui/src/models/profilingQuestionsSlice.ts b/jb-ui/src/models/profilingQuestionsSlice.ts index cc36f17..577566f 100644 --- a/jb-ui/src/models/profilingQuestionsSlice.ts +++ b/jb-ui/src/models/profilingQuestionsSlice.ts @@ -1,313 +1,327 @@ import { - BodySubmitProfilingQuestionsProductIdProfilingQuestionsPost, - ProfilingQuestionsApi, - StatusResponse, - UpkQuestion, UserQuestionAnswerIn + BodySubmitProfilingQuestionsProductIdProfilingQuestionsPost, + ProfilingQuestionsApi, + StatusResponse, + UpkQuestion, + UserQuestionAnswerIn, } from "@/api_fsb"; import { assert, bpid } from "@/lib/utils"; import { questionUtils } from "@/models/profilingUtils"; -import type { RootState } from '@/store'; +import type { RootState } from "@/store"; import { - createAsyncThunk, - createEntityAdapter, createSlice, - PayloadAction, - WritableDraft -} from '@reduxjs/toolkit'; + createAsyncThunk, + createEntityAdapter, + createSlice, + PayloadAction, + WritableDraft, +} from "@reduxjs/toolkit"; interface UpkQuestionMetadata { - // The data we need to conduct profiling questions and manager user - // submission along with the User Interfaces + // The data we need to conduct profiling questions and manager user + // submission along with the User Interfaces - // Only one Question can be active at a time, which is the one - // being shown to the Respondent - isActive: boolean; + // Only one Question can be active at a time, which is the one + // being shown to the Respondent + isActive: boolean; - answers: string[]; + answers: string[]; - // If it's actively being submitted to the server - isProcessing: boolean; + // If it's actively being submitted to the server + isProcessing: boolean; - // If we recieved a successful response from the server after submitting - isSaved: boolean; + // If we recieved a successful response from the server after submitting + isSaved: boolean; } interface ValidationResult { - // If isValid or isComplete are undefined, that means the question - // hasn't been validated yet. This is different from false, which means - // it has been validated and is not valid. - isValid?: boolean; - isComplete?: boolean; - errors: ValidationError[]; -}; + // If isValid or isComplete are undefined, that means the question + // hasn't been validated yet. This is different from false, which means + // it has been validated and is not valid. + isValid?: boolean; + isComplete?: boolean; + errors: ValidationError[]; +} export interface ValidationError { - path: string; // e.g., "user.email" - message: string; - severity: 'error' | 'warning'; -}; + path: string; // e.g., "user.email" + message: string; + severity: "error" | "warning"; +} export interface ProfileQuestion extends UpkQuestion { - _metadata: UpkQuestionMetadata; - _validation: ValidationResult; + _metadata: UpkQuestionMetadata; + _validation: ValidationResult; } // Entity adapter for normalized storage const questionsAdapter = createEntityAdapter({ - selectId: (model: ProfileQuestion) => model.question_id!, - sortComparer: (a, b) => { - return (b.importance?.task_score ?? 0) - (a.importance?.task_score ?? 0); - }, + selectId: (model: ProfileQuestion) => model.question_id!, + sortComparer: (a, b) => { + return (b.importance?.task_score ?? 0) - (a.importance?.task_score ?? 0); + }, }); - function getDefaultMetadata(): UpkQuestionMetadata { - return { - isActive: false, - answers: [], - isProcessing: false, - isSaved: false, - }; + return { + isActive: false, + answers: [], + isProcessing: false, + isSaved: false, + }; } function getDefaultValidation(): ValidationResult { - - return { - isValid: undefined, - isComplete: undefined, - errors: [], - }; + return { + isValid: undefined, + isComplete: undefined, + errors: [], + }; } // Create the async thunk (outside your slice) export const saveAnswer = createAsyncThunk( - 'profilingQuestions/saveAnswer', - async ({ questionId, bpuid }: { questionId: string; bpuid: string }, { getState, dispatch }) => { - const state = getState() as RootState; - const question = state.profilingQuestions.entities[questionId]; - - // Validations - const answers = { - 'question_id': question.question_id!, - 'answer': question._metadata.answers - } as UserQuestionAnswerIn; - - const answers_body = { - 'answers': [answers] - } as BodySubmitProfilingQuestionsProductIdProfilingQuestionsPost; - - // Make API call - const res = await new ProfilingQuestionsApi() - .submitProfilingQuestionsProductIdProfilingQuestionsPost( - bpid, - bpuid, - answers_body, - undefined, - true - ); - - const response = res.data as StatusResponse; - - // Check if we need to fetch new questions BEFORE returning - if (response.status === "success" && isFirstNonGR(state.profilingQuestions)) { - // NOW dispatch it - dispatch(fetchNewQuestions(bpuid)); - } - - return { questionId, response }; - + "profilingQuestions/saveAnswer", + async ( + { questionId, bpuid }: { questionId: string; bpuid: string }, + { getState, dispatch }, + ) => { + const state = getState() as RootState; + const question = state.profilingQuestions.entities[questionId]; + + // Validations + const answers = { + question_id: question.question_id!, + answer: question._metadata.answers, + } as UserQuestionAnswerIn; + + const answers_body = { + answers: [answers], + } as BodySubmitProfilingQuestionsProductIdProfilingQuestionsPost; + + // Make API call + const res = + await new ProfilingQuestionsApi().submitProfilingQuestionsProductIdProfilingQuestionsPost( + bpid, + bpuid, + answers_body, + undefined, + true, + ); + + const response = res.data as StatusResponse; + + // Check if we need to fetch new questions BEFORE returning + if ( + response.status === "success" && + isFirstNonGR(state.profilingQuestions) + ) { + // NOW dispatch it + dispatch(fetchNewQuestions(bpuid)); } -) -export const fetchNewQuestions = createAsyncThunk( - 'profilingQuestions/fetchNewQuestions', - async (bpuid: string) => { - - console.log("profilingQuestions/fetchNewQuestions") - const res = await new ProfilingQuestionsApi() - .getProfilingQuestionsProductIdProfilingQuestionsGet( - bpid, - bpuid, - undefined, // "104.9.125.144", // ip - undefined, // countryIso - undefined, // languageIso - 2_500 - ); - return res.data.questions; - } + return { questionId, response }; + }, ); -type ProfilingQuestionsState = ReturnType<typeof profilingQuestionSlice.getInitialState>; +export const fetchNewQuestions = createAsyncThunk( + "profilingQuestions/fetchNewQuestions", + async (bpuid: string) => { + console.log("profilingQuestions/fetchNewQuestions"); + const res = + await new ProfilingQuestionsApi().getProfilingQuestionsProductIdProfilingQuestionsGet( + bpid, + bpuid, + undefined, // "104.9.125.144", // ip + undefined, // countryIso + undefined, // languageIso + 2_500, + ); + return res.data.questions; + }, +); +type ProfilingQuestionsState = ReturnType< + typeof profilingQuestionSlice.getInitialState +>; // Helper function that contains the logic (outside the slice) function goToNextQuestionLogic(state: WritableDraft<ProfilingQuestionsState>) { - const allQuestions = questionsAdapter.getSelectors().selectAll(state); - const currentIndex = allQuestions.findIndex(q => q._metadata.isActive); - - if (currentIndex === -1) return; - - const totalQuestions = allQuestions.length; - let searchIndex = (currentIndex + 1) % totalQuestions; - let iterations = 0; - - while (iterations < totalQuestions) { - if (!allQuestions[searchIndex]._metadata.isSaved) { - questionsAdapter.updateMany(state, [ - { - id: allQuestions[currentIndex].question_id!, - changes: { - _metadata: { - ...allQuestions[currentIndex]._metadata, - isActive: false - } - } - }, - { - id: allQuestions[searchIndex].question_id!, - changes: { - _metadata: { - ...allQuestions[searchIndex]._metadata, - isActive: true - } - } - } - ]); - return; - } - - searchIndex = (searchIndex + 1) % totalQuestions; - iterations++; + const allQuestions = questionsAdapter.getSelectors().selectAll(state); + const currentIndex = allQuestions.findIndex((q) => q._metadata.isActive); + + if (currentIndex === -1) return; + + const totalQuestions = allQuestions.length; + let searchIndex = (currentIndex + 1) % totalQuestions; + let iterations = 0; + + while (iterations < totalQuestions) { + if (!allQuestions[searchIndex]._metadata.isSaved) { + questionsAdapter.updateMany(state, [ + { + id: allQuestions[currentIndex].question_id!, + changes: { + _metadata: { + ...allQuestions[currentIndex]._metadata, + isActive: false, + }, + }, + }, + { + id: allQuestions[searchIndex].question_id!, + changes: { + _metadata: { + ...allQuestions[searchIndex]._metadata, + isActive: true, + }, + }, + }, + ]); + return; } + + searchIndex = (searchIndex + 1) % totalQuestions; + iterations++; + } } function setProfilingQuestionsLogic( - state: WritableDraft<ProfilingQuestionsState>, - questions: ProfileQuestion[]) { - - const hasActiveQuestion = Object.values(state.entities).some( - entity => entity?._metadata.isActive - ); - - const entities = questions.map((serverQuestion, index) => { - const existing = state.entities[serverQuestion.question_id!]; - const shouldActivate = !hasActiveQuestion && index === 0; - - return { - ...serverQuestion, - _metadata: existing?._metadata || { - ...getDefaultMetadata(), - isActive: shouldActivate, - }, - _validation: existing?._validation || getDefaultValidation(), - }; - }); + state: WritableDraft<ProfilingQuestionsState>, + questions: ProfileQuestion[], +) { + const hasActiveQuestion = Object.values(state.entities).some( + (entity) => entity?._metadata.isActive, + ); - questionsAdapter.setAll(state, entities); -} + const entities = questions.map((serverQuestion, index) => { + const existing = state.entities[serverQuestion.question_id!]; + const shouldActivate = !hasActiveQuestion && index === 0; + return { + ...serverQuestion, + _metadata: existing?._metadata || { + ...getDefaultMetadata(), + isActive: shouldActivate, + }, + _validation: existing?._validation || getDefaultValidation(), + }; + }); + + questionsAdapter.setAll(state, entities); +} function isGR(ext_question_id: string): boolean { - return ext_question_id.startsWith("gr:") + return ext_question_id.startsWith("gr:"); } function isFirstNonGR(state: WritableDraft<ProfilingQuestionsState>): boolean { - // We want to identify if the next question is the first non-GR question - // because will want to trigger a full profile question refresh. + // We want to identify if the next question is the first non-GR question + // because will want to trigger a full profile question refresh. - const allQuestions = questionsAdapter.getSelectors().selectAll(state); - const currentIndex = allQuestions.findIndex(q => q._metadata.isActive); + const allQuestions = questionsAdapter.getSelectors().selectAll(state); + const currentIndex = allQuestions.findIndex((q) => q._metadata.isActive); - if (currentIndex === -1) false; + if (currentIndex === -1) false; - const totalQuestions = allQuestions.length; - let searchIndex = (currentIndex + 1) % totalQuestions; - let iterations = 0; + const totalQuestions = allQuestions.length; + let searchIndex = (currentIndex + 1) % totalQuestions; + let iterations = 0; - while (iterations < totalQuestions) { - if (!allQuestions[searchIndex]._metadata.isSaved) { + while (iterations < totalQuestions) { + if (!allQuestions[searchIndex]._metadata.isSaved) { + const current = isGR(allQuestions[currentIndex].ext_question_id!); + const next = isGR(allQuestions[searchIndex].ext_question_id!); - const current = isGR(allQuestions[currentIndex].ext_question_id!) - const next = isGR(allQuestions[searchIndex].ext_question_id!) - - // We want to identify transitions from GR to non-GR questions, - // as we use GR questions to pre-calculate some non-GR questions, - // we should force a refresh. - return current && !next; - } - - searchIndex = (searchIndex + 1) % totalQuestions; - iterations++; + // We want to identify transitions from GR to non-GR questions, + // as we use GR questions to pre-calculate some non-GR questions, + // we should force a refresh. + return current && !next; } - return false; + searchIndex = (searchIndex + 1) % totalQuestions; + iterations++; + } + + return false; } const profilingQuestionSlice = createSlice({ - name: 'profilingQuestions', - initialState: questionsAdapter.getInitialState(), - reducers: { + name: "profilingQuestions", + initialState: questionsAdapter.getInitialState(), + reducers: { + setProfilingQuestions: ( + state, + action: PayloadAction<ProfileQuestion[]>, + ) => { + setProfilingQuestionsLogic(state, action.payload); + }, - setProfilingQuestions: (state, action: PayloadAction<ProfileQuestion[]>) => { - setProfilingQuestionsLogic(state, action.payload); - }, + metadataUpdated: ( + state, + action: PayloadAction<{ + question_id: string; + metadata: Partial<UpkQuestionMetadata>; + }>, + ) => { + const { question_id, metadata } = action.payload; + const entity = state.entities[question_id]; + if (entity) { + entity._metadata = { ...entity._metadata, ...metadata }; + } + }, - metadataUpdated: ( - state, - action: PayloadAction<{ question_id: string; metadata: Partial<UpkQuestionMetadata> }> - ) => { - const { question_id, metadata } = action.payload; - const entity = state.entities[question_id]; - if (entity) { - entity._metadata = { ...entity._metadata, ...metadata }; - } - }, + validationUpdated: ( + state, + action: PayloadAction<{ + question_id: string; + validation: Partial<ValidationResult>; + }>, + ) => { + const { question_id, validation } = action.payload; + const entity = state.entities[question_id]; + if (entity) { + entity._validation = { ...entity._validation, ...validation }; + } + }, - validationUpdated: ( - state, - action: PayloadAction<{ question_id: string; validation: Partial<ValidationResult> }> - ) => { - const { question_id, validation } = action.payload; - const entity = state.entities[question_id]; - if (entity) { - entity._validation = { ...entity._validation, ...validation }; - } - }, + // Set one question as active, deactivate all others + setActiveQuestion: (state, action: PayloadAction<string>) => { + const questionId = action.payload; - // Set one question as active, deactivate all others - setActiveQuestion: (state, action: PayloadAction<string>) => { - const questionId = action.payload; - - // Deactivate all questions - Object.values(state.entities).forEach(entity => { - if (entity) { - entity._metadata.isActive = false; - } - }); - - // Activate the selected one - const entity = state.entities[questionId]; - if (entity) { - entity._metadata.isActive = true; - } - }, + // Deactivate all questions + Object.values(state.entities).forEach((entity) => { + if (entity) { + entity._metadata.isActive = false; + } + }); - // Clear active state from all questions - clearActiveQuestion: (state) => { - Object.values(state.entities).forEach(entity => { - if (entity) { - entity._metadata.isActive = false; - } - }); - }, + // Activate the selected one + const entity = state.entities[questionId]; + if (entity) { + entity._metadata.isActive = true; + } + }, - goToNextQuestion: (state) => { - goToNextQuestionLogic(state); - }, + // Clear active state from all questions + clearActiveQuestion: (state) => { + Object.values(state.entities).forEach((entity) => { + if (entity) { + entity._metadata.isActive = false; + } + }); + }, + + goToNextQuestion: (state) => { + goToNextQuestionLogic(state); + }, - // ----------------------- + // ----------------------- - addAnswers(state, action: PayloadAction<{ questionId: string, answers: string[] }>) { - /* When changing the answers in anyway, we want to: + addAnswers( + state, + action: PayloadAction<{ questionId: string; answers: string[] }>, + ) { + /* When changing the answers in anyway, we want to: 1. Add the new answers to the question metadata This does not perform validation, it simply reassigns @@ -325,112 +339,124 @@ const profilingQuestionSlice = createSlice({ 3. Re-calculate the isComplete and isValid state */ - // let new_question = questionUtils.addAnswer(action.payload.question, action.payload.answers) - let entity = state.entities[action.payload.questionId]; - const newMetadata = { ...entity._metadata, answers: action.payload.answers }; + // let new_question = questionUtils.addAnswer(action.payload.question, action.payload.answers) + let entity = state.entities[action.payload.questionId]; + const newMetadata = { + ...entity._metadata, + answers: action.payload.answers, + }; - if (entity) { - entity._metadata = newMetadata; + if (entity) { + entity._metadata = newMetadata; - entity = questionUtils.validate(entity) - entity = questionUtils.assignValidationState(entity) + entity = questionUtils.validate(entity); + entity = questionUtils.assignValidationState(entity); - state.entities[action.payload.questionId] = entity - } - }, + state.entities[action.payload.questionId] = entity; + } }, - - extraReducers: (builder) => { - builder - .addCase(saveAnswer.pending, (state, action) => { - const questionId = action.meta.arg.questionId; - const bpuid = action.meta.arg.bpuid; - assert(bpuid, "Worker must be defined"); - - const question = state.entities[questionId]; - if (question) { - assert(question._validation.isComplete, "Must submit Completed Questions"); - assert(!question._metadata.isProcessing, "Can't submit processing Answer"); - assert(!question._metadata.isSaved, "Can't submit completed Answer"); - question._metadata.isProcessing = true; - } - }) - .addCase(saveAnswer.fulfilled, (state, action) => { - const { questionId, response } = action.payload; - const question = state.entities[questionId]; - - if (question) { - question._metadata.isProcessing = false; - if (response.status === "success") { - question._metadata.isSaved = true; - - // fetchNewQuestions is already being dispatched from - // the THUNK - goToNextQuestionLogic(state); - - } else { - question._metadata.isSaved = false; - } - } - }) - .addCase(saveAnswer.rejected, (state, action) => { - const questionId = action.meta.arg.questionId; - const question = state.entities[questionId]; - - if (question) { - question._metadata.isProcessing = false; - question._metadata.isSaved = false; - } - }) - .addCase(fetchNewQuestions.fulfilled, (state, action) => { - console.log(".addCase(fetchNewQuestions.fulfilled, (state, action) => {") - setProfilingQuestionsLogic(state, action.payload as ProfileQuestion[]); - }); - ; - } - -}) + }, + + extraReducers: (builder) => { + builder + .addCase(saveAnswer.pending, (state, action) => { + const questionId = action.meta.arg.questionId; + const bpuid = action.meta.arg.bpuid; + assert(bpuid, "Worker must be defined"); + + const question = state.entities[questionId]; + if (question) { + assert( + question._validation.isComplete, + "Must submit Completed Questions", + ); + assert( + !question._metadata.isProcessing, + "Can't submit processing Answer", + ); + assert(!question._metadata.isSaved, "Can't submit completed Answer"); + question._metadata.isProcessing = true; + } + }) + .addCase(saveAnswer.fulfilled, (state, action) => { + const { questionId, response } = action.payload; + const question = state.entities[questionId]; + + if (question) { + question._metadata.isProcessing = false; + if (response.status === "success") { + question._metadata.isSaved = true; + + // fetchNewQuestions is already being dispatched from + // the THUNK + goToNextQuestionLogic(state); + } else { + question._metadata.isSaved = false; + } + } + }) + .addCase(saveAnswer.rejected, (state, action) => { + const questionId = action.meta.arg.questionId; + const question = state.entities[questionId]; + + if (question) { + question._metadata.isProcessing = false; + question._metadata.isSaved = false; + } + }) + .addCase(fetchNewQuestions.fulfilled, (state, action) => { + console.log( + ".addCase(fetchNewQuestions.fulfilled, (state, action) => {", + ); + setProfilingQuestionsLogic(state, action.payload as ProfileQuestion[]); + }); + }, +}); export const { - setProfilingQuestions, - metadataUpdated, - setActiveQuestion, - goToNextQuestion, - addAnswers, + setProfilingQuestions, + metadataUpdated, + setActiveQuestion, + goToNextQuestion, + addAnswers, } = profilingQuestionSlice.actions; // Selectors export const questionsSelectors = questionsAdapter.getSelectors( - (state: RootState) => state.profilingQuestions + (state: RootState) => state.profilingQuestions, ); // Custom selector to get the active question export const selectAllQuestions = (state: RootState): ProfileQuestion[] => { - return questionsSelectors.selectAll(state) ?? []; + return questionsSelectors.selectAll(state) ?? []; }; // Custom selector to get the active question -export const selectActiveQuestion = (state: RootState): ProfileQuestion | null => { - const allQuestions = questionsSelectors.selectAll(state); - return allQuestions.find(q => q._metadata.isActive) || null; +export const selectActiveQuestion = ( + state: RootState, +): ProfileQuestion | null => { + const allQuestions = questionsSelectors.selectAll(state); + return allQuestions.find((q) => q._metadata.isActive) || null; }; // Custom selector to get active question ID export const selectActiveQuestionId = (state: RootState): string | null => { - const activeQuestion = selectActiveQuestion(state); - return activeQuestion?.question_id || null; + const activeQuestion = selectActiveQuestion(state); + return activeQuestion?.question_id || null; }; -export const selectActiveQuestionMetadata = (state: RootState): UpkQuestionMetadata | null => { - const activeQuestion = selectActiveQuestion(state); - return activeQuestion?._metadata || null; +export const selectActiveQuestionMetadata = ( + state: RootState, +): UpkQuestionMetadata | null => { + const activeQuestion = selectActiveQuestion(state); + return activeQuestion?._metadata || null; }; -export const selectActiveQuestionValidation = (state: RootState): ValidationResult | null => { - const activeQuestion = selectActiveQuestion(state); - return activeQuestion?._validation || null; +export const selectActiveQuestionValidation = ( + state: RootState, +): ValidationResult | null => { + const activeQuestion = selectActiveQuestion(state); + return activeQuestion?._validation || null; }; - -export default profilingQuestionSlice.reducer - +export default profilingQuestionSlice.reducer; diff --git a/jb-ui/src/models/profilingUtils.ts b/jb-ui/src/models/profilingUtils.ts index 104a5bb..a3ef7d2 100644 --- a/jb-ui/src/models/profilingUtils.ts +++ b/jb-ui/src/models/profilingUtils.ts @@ -1,29 +1,36 @@ -import { PatternValidation, UpkQuestionConfigurationMC, UpkQuestionConfigurationTE } from "@/api_fsb"; +import { + PatternValidation, + UpkQuestionConfigurationMC, + UpkQuestionConfigurationTE, +} from "@/api_fsb"; import { assert } from "@/lib/utils"; -import { ProfileQuestion, ValidationError } from '@/models/profilingQuestionsSlice'; -import { Filter } from 'bad-words'; +import { + ProfileQuestion, + ValidationError, +} from "@/models/profilingQuestionsSlice"; +import { Filter } from "bad-words"; const filter = new Filter(); export const questionUtils = { - - assignValidationState: (question: ProfileQuestion): ProfileQuestion => { - /* This function performs validation on the question's current answers and + assignValidationState: (question: ProfileQuestion): ProfileQuestion => { + /* This function performs validation on the question's current answers and assigns any error messages to question._metadata.error_msg. If the question is MC, validate: */ - const errors = question._validation.errors ?? [] + const errors = question._validation.errors ?? []; - question._validation.isComplete = errors.length === 0 - question._validation.isValid = errors.filter(e => e.severity === 'error').length === 0 + question._validation.isComplete = errors.length === 0; + question._validation.isValid = + errors.filter((e) => e.severity === "error").length === 0; - return question; - }, + return question; + }, - validate: (question: ProfileQuestion): ProfileQuestion => { - /* If the question is MC, validate: + validate: (question: ProfileQuestion): ProfileQuestion => { + /* If the question is MC, validate: - validate selector SA vs MA (1 selected vs >1 selected) - the answers match actual codes in the choices @@ -35,121 +42,132 @@ export const questionUtils = { - validation.patterns */ - assert(question.question_id, "Question must have question_id") - // assert(question.configuration, "Question must have configuration") - const answers = question._metadata.answers ?? [] - - // Declare it here, and they we'll reassign any specific - // configuration types based on the question type. - - // Start fresh without any error messages before validating - const errors: ValidationError[] = [] - - switch (question.question_type) { - case "TE": - - answers.length === 0 && errors.push({ - path: `question_id:${question.question_id}`, - message: "No answer provided", - severity: "warning", - } as ValidationError) - - answers.length > 1 && errors.push({ - path: `question_id:${question.question_id}`, - message: "Only one answer allowed", - severity: "error", - } as ValidationError) - - let answer_text: string = answers[0] - - const te_config = question.configuration as UpkQuestionConfigurationTE - const min_length = te_config.min_length ?? 0 - const max_length = te_config.max_length ?? 100_000 - - answer_text.length < min_length && errors.push({ - path: `question_id:${question.question_id}`, - message: "Answer shorter than allowed", - severity: "warning", - } as ValidationError) - - answer_text.length > max_length && errors.push({ - path: `question_id:${question.question_id}`, - message: "Answer longer than allowed", - severity: "error", - } as ValidationError) - - filter.isProfane(answer_text) && errors.push({ - path: `question_id:${question.question_id}`, - message: "Answer has inappropriate content", - severity: "error", - } as ValidationError) - - const patterns: PatternValidation[] = (question.validation ?? {})["patterns"] ?? [] - patterns.forEach((pv) => { - let re = new RegExp(pv.pattern) - answer_text.search(re) == -1 && errors.push({ - path: `question_id:${question.question_id}`, - message: pv.message, - severity: "error", - } as ValidationError) - - }) - - break; - - case "MC": - - answers.length === 0 && errors.push({ - path: `question_id:${question.question_id}`, - message: "Multiple Choice question with no selected answers", - severity: "warning", - } as ValidationError) - - const choice_codes: string[] = question.choices?.map((c) => c.choice_id) ?? []; - - const mc_config = (question.configuration ?? {}) as UpkQuestionConfigurationMC - const max_select = mc_config.max_select ?? choice_codes.length - - switch (question.selector) { - case "SA": - answers.length > 1 && errors.push({ - path: `question_id:${question.question_id}`, - message: "Single Answer MC question with >1 selected answers", - severity: "error", - } as ValidationError) - - break; - - case "MA": - answers.length > max_select && errors.push({ - path: `question_id:${question.question_id}`, - message: "More options selected than allowed", - severity: "error", - } as ValidationError) - - answers.length === max_select && errors.push({ - path: `question_id:${question.question_id}`, - message: "Selected all the options", - severity: "error", - } as ValidationError) - - break; - } - - // Now validate Multiple Choice answers regardless of if - // they're Single Answer (SA) or Multiple Answer (MA) - !answers.every(item => choice_codes.includes(item)) && errors.push({ - path: `question_id:${question.question_id}`, - message: "Invalid options selected", - severity: "error", - }) - - // const max_select: number = question.configuration?.max_select ?? choice_codes.length - // if (answer.values.length > max_select) { - // answer.error_msg = "More options selected than allowed" - // } - - /* + assert(question.question_id, "Question must have question_id"); + // assert(question.configuration, "Question must have configuration") + const answers = question._metadata.answers ?? []; + + // Declare it here, and they we'll reassign any specific + // configuration types based on the question type. + + // Start fresh without any error messages before validating + const errors: ValidationError[] = []; + + switch (question.question_type) { + case "TE": + answers.length === 0 && + errors.push({ + path: `question_id:${question.question_id}`, + message: "No answer provided", + severity: "warning", + } as ValidationError); + + answers.length > 1 && + errors.push({ + path: `question_id:${question.question_id}`, + message: "Only one answer allowed", + severity: "error", + } as ValidationError); + + let answer_text: string = answers[0]; + + const te_config = question.configuration as UpkQuestionConfigurationTE; + const min_length = te_config.min_length ?? 0; + const max_length = te_config.max_length ?? 100_000; + + answer_text.length < min_length && + errors.push({ + path: `question_id:${question.question_id}`, + message: "Answer shorter than allowed", + severity: "warning", + } as ValidationError); + + answer_text.length > max_length && + errors.push({ + path: `question_id:${question.question_id}`, + message: "Answer longer than allowed", + severity: "error", + } as ValidationError); + + filter.isProfane(answer_text) && + errors.push({ + path: `question_id:${question.question_id}`, + message: "Answer has inappropriate content", + severity: "error", + } as ValidationError); + + const patterns: PatternValidation[] = + (question.validation ?? {})["patterns"] ?? []; + patterns.forEach((pv) => { + let re = new RegExp(pv.pattern); + answer_text.search(re) == -1 && + errors.push({ + path: `question_id:${question.question_id}`, + message: pv.message, + severity: "error", + } as ValidationError); + }); + + break; + + case "MC": + answers.length === 0 && + errors.push({ + path: `question_id:${question.question_id}`, + message: "Multiple Choice question with no selected answers", + severity: "warning", + } as ValidationError); + + const choice_codes: string[] = + question.choices?.map((c) => c.choice_id) ?? []; + + const mc_config = (question.configuration ?? + {}) as UpkQuestionConfigurationMC; + const max_select = mc_config.max_select ?? choice_codes.length; + + switch (question.selector) { + case "SA": + answers.length > 1 && + errors.push({ + path: `question_id:${question.question_id}`, + message: "Single Answer MC question with >1 selected answers", + severity: "error", + } as ValidationError); + + break; + + case "MA": + answers.length > max_select && + errors.push({ + path: `question_id:${question.question_id}`, + message: "More options selected than allowed", + severity: "error", + } as ValidationError); + + answers.length === max_select && + errors.push({ + path: `question_id:${question.question_id}`, + message: "Selected all the options", + severity: "error", + } as ValidationError); + + break; + } + + // Now validate Multiple Choice answers regardless of if + // they're Single Answer (SA) or Multiple Answer (MA) + !answers.every((item) => choice_codes.includes(item)) && + errors.push({ + path: `question_id:${question.question_id}`, + message: "Invalid options selected", + severity: "error", + }); + + // const max_select: number = question.configuration?.max_select ?? choice_codes.length + // if (answer.values.length > max_select) { + // answer.error_msg = "More options selected than allowed" + // } + + /* exclusive_choice = next((x for x in question["choices"] if x.get("exclusive")), None) if exclusive_choice: exclusive_choice_id = exclusive_choice["choice_id"] @@ -157,21 +175,20 @@ export const questionUtils = { exclusive_choice_id not in answer, "Invalid exclusive selection" */ - break; - - default: - errors.push({ - path: `question_id:${question.question_id}`, - message: "Incorrect Question Type provided", - severity: "error", - } as ValidationError) - } - - // Assign the errors back to the question's _validation field and - // then return the question - // TODO: does this edit in place, maybe I don't need to return it - question._validation.errors = errors - return question; - }, - -};
\ No newline at end of file + break; + + default: + errors.push({ + path: `question_id:${question.question_id}`, + message: "Incorrect Question Type provided", + severity: "error", + } as ValidationError); + } + + // Assign the errors back to the question's _validation field and + // then return the question + // TODO: does this edit in place, maybe I don't need to return it + question._validation.errors = errors; + return question; + }, +}; diff --git a/jb-ui/src/pages/Home.tsx b/jb-ui/src/pages/Home.tsx index 5a5c953..8d509b6 100644 --- a/jb-ui/src/pages/Home.tsx +++ b/jb-ui/src/pages/Home.tsx @@ -1,13 +1,7 @@ -import max_lulu from "./max-and-lulu.jpg" +import max_lulu from "./max-and-lulu.jpg"; const Home = function () { - return ( - <img - src={max_lulu} - alt="Max and Lulu" - className="object-contain" - /> - ) + return <img src={max_lulu} alt="Max and Lulu" className="object-contain" />; }; -export default Home;
\ No newline at end of file +export default Home; diff --git a/jb-ui/src/pages/MagicLink.tsx b/jb-ui/src/pages/MagicLink.tsx new file mode 100644 index 0000000..eb272a6 --- /dev/null +++ b/jb-ui/src/pages/MagicLink.tsx @@ -0,0 +1,46 @@ +import { useEffect, useRef, useState } from "react"; + +type ExchangeState = "working" | "failed" | "missing"; + +const MagicLink = function () { + const started = useRef(false); + const [state, setState] = useState<ExchangeState>("working"); + + useEffect(() => { + // React Strict Mode runs effects twice in development. Never redeem twice. + if (started.current) return; + started.current = true; + + const params = new URLSearchParams(window.location.search); + const token = params.get("token"); + if (!token) { + setState("missing"); + return; + } + + // Keep the credential out of browser history and subsequent Referer headers. + window.history.replaceState({}, "", window.location.pathname); + + void fetch("/auth/magic-link/exchange", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token: token }), + }) + .then((response) => { + if (!response.ok) throw new Error("Magic-link exchange failed"); + window.location.replace("/"); + }) + .catch(() => setState("failed")); + }, []); + + if (state === "missing") { + return <p>This login link is missing its token.</p>; + } + if (state === "failed") { + return <p>This login link is invalid or has expired.</p>; + } + return <p>Signing you in…</p>; +}; + +export default MagicLink; diff --git a/jb-ui/src/pages/Preview.tsx b/jb-ui/src/pages/Preview.tsx index 153b4bb..c58a85e 100644 --- a/jb-ui/src/pages/Preview.tsx +++ b/jb-ui/src/pages/Preview.tsx @@ -1,287 +1,394 @@ import { - Leaderboard, - LeaderboardApi, - LeaderboardCode, - LeaderboardFrequency, - LeaderboardRow, + Leaderboard, + LeaderboardApi, + LeaderboardCode, + LeaderboardFrequency, + LeaderboardRow, } from "@/api_fsb"; import { useAppSelector } from "@/hooks"; -import { bpid, formatCentsToUSD, truncate, } from "@/lib/utils"; +import { bpid, formatCentsToUSD, truncate } from "@/lib/utils"; import { activeSurveys, activeUsers, maxPayout } from "@/models/grlStatsSlice"; import { clsx } from "clsx"; -import moment from 'moment'; -import { useEffect, useState } from 'react'; +import moment from "moment"; +import { FormEvent, useEffect, useState } from "react"; const showSmartRank = ( - item: LeaderboardRow, - index: number, - items: LeaderboardRow[] | undefined + item: LeaderboardRow, + index: number, + items: LeaderboardRow[] | undefined, ): boolean => { - /** - * Smart rank calculation - this determines if we should show the rank - * value. It's confusing to show people that multiple individuals ranked - * in the same place, so this will only display the next ranked value, eg: - * - * - 1st - * - ___ - * - 3rd - * - ___ - * - ___ - * - 6th - * - * @returns number - */ - if (!items) return false; - - const thisRank = item.rank; - if (index >= 1) { - const prevRank = items[index - 1].rank; - if (thisRank === prevRank) { - return false; - } + /** + * Smart rank calculation - this determines if we should show the rank + * value. It's confusing to show people that multiple individuals ranked + * in the same place, so this will only display the next ranked value, eg: + * + * - 1st + * - ___ + * - 3rd + * - ___ + * - ___ + * - 6th + * + * @returns number + */ + if (!items) return false; + + const thisRank = item.rank; + if (index >= 1) { + const prevRank = items[index - 1].rank; + if (thisRank === prevRank) { + return false; } - return true; + } + return true; }; interface FrequencyButtonsProps { - selected: LeaderboardFrequency; - onChange: (frequency: LeaderboardFrequency) => void; + selected: LeaderboardFrequency; + onChange: (frequency: LeaderboardFrequency) => void; } const frequency_map = [ - { label: 'Day', value: LeaderboardFrequency.Daily }, - { label: 'Week', value: LeaderboardFrequency.Weekly }, - { label: 'Month', value: LeaderboardFrequency.Monthly }, + { label: "Day", value: LeaderboardFrequency.Daily }, + { label: "Week", value: LeaderboardFrequency.Weekly }, + { label: "Month", value: LeaderboardFrequency.Monthly }, ]; const ThreeButtonToggle = ({ selected, onChange }: FrequencyButtonsProps) => { - return ( - <div className="inline-flex rounded shadow-sm" role="group"> - {frequency_map.map((button, idx) => ( - <button - type="button" - key={button.value} - onClick={() => onChange(button.value)} - className={clsx( - "px-2 py-1 text-[10px] font-medium", - "border-t border-b border-gray-200", - "hover:cursor-pointer hover:bg-zinc-200 hover:text-zinc-950", - idx === 0 && "rounded-l", - idx === frequency_map.length - 1 && "rounded-r", - selected === button.value - ? 'text-zinc-50 bg-zinc-400' - : 'text-zinc-950 bg-zinc-50', - )}> - {button.label} - </button> - ))} - </div> - ); + return ( + <div className="inline-flex rounded shadow-sm" role="group"> + {frequency_map.map((button, idx) => ( + <button + type="button" + key={button.value} + onClick={() => onChange(button.value)} + className={clsx( + "px-2 py-1 text-[10px] font-medium", + "border-t border-b border-gray-200", + "hover:cursor-pointer hover:bg-zinc-200 hover:text-zinc-950", + idx === 0 && "rounded-l", + idx === frequency_map.length - 1 && "rounded-r", + selected === button.value + ? "text-zinc-50 bg-zinc-400" + : "text-zinc-950 bg-zinc-50", + )} + > + {button.label} + </button> + ))} + </div> + ); }; - - // Leaderboards Component export const Leaderboards = () => { - /** - * Leaderboards do not do any caching, or better state management. This is okay because - * the API is handled to take it. However, it can easily handled better client - * side rather than making the - * **/ - - const [frequency, setFrequency] = useState<LeaderboardFrequency>( - LeaderboardFrequency.Daily + /** + * Leaderboards do not do any caching, or better state management. This is okay because + * the API is handled to take it. However, it can easily handled better client + * side rather than making the + * **/ + + const [frequency, setFrequency] = useState<LeaderboardFrequency>( + LeaderboardFrequency.Daily, + ); + + const [payouts, setPayouts] = useState<Leaderboard | null>(null); + const [completes, setCompletes] = useState<Leaderboard | null>(null); + + const bpuid = undefined; + + useEffect(() => { + new LeaderboardApi() + .timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet( + bpid, + LeaderboardCode.CompleteCount, + frequency, + "us", + bpuid, + undefined, // within_time + 10, + ) + .then((res) => { + setCompletes(res.data.leaderboard as Leaderboard); + }); + + new LeaderboardApi() + .timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet( + bpid, + LeaderboardCode.SumUserPayout, + frequency, + "us", + bpuid, + undefined, + 10, + ) + .then((res) => { + setPayouts(res.data.leaderboard as Leaderboard); + }); + }, [frequency]); + + let dateRange = " - "; + if (completes) { + const start = moment(completes?.start_timestamp * 1000).format( + "MMM Do, ha", ); - - const [payouts, setPayouts] = useState<Leaderboard | null>(null); - const [completes, setCompletes] = useState<Leaderboard | null>(null); - - // const bpuid = useAppSelector(state => state.app.bpuid) - const bpuid = undefined - - useEffect(() => { - new LeaderboardApi().timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet( - bpid, - LeaderboardCode.CompleteCount, - frequency, - "us", - bpuid, // bpuid - undefined, // within_time - 10) - .then(res => { - setCompletes(res.data.leaderboard as Leaderboard) - }) - - new LeaderboardApi().timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet( - bpid, - LeaderboardCode.SumUserPayout, - frequency, - "us", - bpuid, - undefined, - 10) - .then(res => { - setPayouts(res.data.leaderboard as Leaderboard) - }) - }, [frequency]); - - let dateRange = " - " - if (completes) { - const start = moment(completes?.start_timestamp * 1000).format("MMM Do, ha"); - const end = moment(completes?.end_timestamp * 1000).format("MMM Do, h:mm:ssa"); - //dateRange = `${start} – ${end} (${completes?.timezone_name})`; - dateRange = `${start} – ${end}`; - } - - const freq_label = frequency_map.find(f => f.value === frequency)?.label.toLocaleLowerCase() - - return ( - <div className="my-2 mx-4 py-0"> - <div className="w-full flex items-center justify-between my-1"> - {/* Left spacer (hidden on small screens) */} - <div className="hidden md:block md:w-1/3"></div> - - {/* Centered text */} - <h3 className="text-center flex-1 md:1/3"> - Leaderboards - </h3> - - {/* Right-aligned button group */} - <div className="flex md:w-1/3 justify-end"> - <ThreeButtonToggle - selected={frequency} - onChange={setFrequency} - /> - </div> - </div> - - <div className="grid grid-cols-1 mb-2 text-zinc-700"> - <p className="text-center text-[10px] font-thin"> - {dateRange} - </p> - </div> - - <div className="grid grid-cols-1 gap-4 px-12 md:px-0 - md:grid-cols-2"> - <table className="border-collapse text-emerald-950 text-[8px] md:text-sm"> - <caption className="caption-top text-xs font-semibold"> - Most earned in the past {freq_label}. - </caption> - <thead> - <tr className=" + const end = moment(completes?.end_timestamp * 1000).format( + "MMM Do, h:mm:ssa", + ); + //dateRange = `${start} – ${end} (${completes?.timezone_name})`; + dateRange = `${start} – ${end}`; + } + + const freq_label = frequency_map + .find((f) => f.value === frequency) + ?.label.toLocaleLowerCase(); + + return ( + <div className="my-2 mx-4 py-0"> + <div className="w-full flex items-center justify-between my-1"> + {/* Left spacer (hidden on small screens) */} + <div className="hidden md:block md:w-1/3"></div> + + {/* Centered text */} + <h3 className="text-center flex-1 md:1/3">Leaderboards</h3> + + {/* Right-aligned button group */} + <div className="flex md:w-1/3 justify-end"> + <ThreeButtonToggle selected={frequency} onChange={setFrequency} /> + </div> + </div> + + <div className="grid grid-cols-1 mb-2 text-zinc-700"> + <p className="text-center text-[10px] font-thin">{dateRange}</p> + </div> + + <div + className="grid grid-cols-1 gap-4 px-12 md:px-0 + md:grid-cols-2" + > + <table className="border-collapse text-emerald-950 text-[8px] md:text-sm"> + <caption className="caption-top text-xs font-semibold"> + Most earned in the past {freq_label}. + </caption> + <thead> + <tr + className=" font-mono font-semibold text-center - border border-zinc-150"> - <th>Rank</th> - <th>Bonus</th> - <th>User</th> - </tr> - </thead> - <tbody className="font-mono"> - - {payouts?.rows?.map((dataItem: LeaderboardRow) => ( - <tr key={dataItem.bpuid} className=" + border border-zinc-150" + > + <th>Rank</th> + <th>Bonus</th> + <th>User</th> + </tr> + </thead> + <tbody className="font-mono"> + {payouts?.rows?.map((dataItem: LeaderboardRow) => ( + <tr + key={dataItem.bpuid} + className=" text-center - border border-zinc-100"> - <td className="font-medium"> - {dataItem.rank} - </td> - <td className="font-semibold"> - {formatCentsToUSD(dataItem.value)} - </td> - <td className="font-light"> - {truncate(dataItem.bpuid, 6, "*****").toUpperCase()} - </td> - </tr> - ))} - </tbody> - </table> - - <table className="border-collapse border border-gray-400 - text-emerald-950 text-[8px] md:text-sm"> - <caption className="caption-top text-xs font-semibold"> - Top completes in the past {freq_label}. - </caption> - <thead> - <tr className=" + border border-zinc-100" + > + <td className="font-medium">{dataItem.rank}</td> + <td className="font-semibold"> + {formatCentsToUSD(dataItem.value)} + </td> + <td className="font-light"> + {truncate(dataItem.bpuid, 6, "*****").toUpperCase()} + </td> + </tr> + ))} + </tbody> + </table> + + <table + className="border-collapse border border-gray-400 + text-emerald-950 text-[8px] md:text-sm" + > + <caption className="caption-top text-xs font-semibold"> + Top completes in the past {freq_label}. + </caption> + <thead> + <tr + className=" font-mono font-semibold text-center - border border-zinc-150 rounded"> - <th>Rank</th> - <th>Completes</th> - <th>User</th> - </tr> - </thead> - <tbody className="font-mono"> - - {completes?.rows?.map((dataItem: LeaderboardRow, index: number) => ( - <tr key={dataItem.bpuid} className=" + border border-zinc-150 rounded" + > + <th>Rank</th> + <th>Completes</th> + <th>User</th> + </tr> + </thead> + <tbody className="font-mono"> + {completes?.rows?.map((dataItem: LeaderboardRow, index: number) => ( + <tr + key={dataItem.bpuid} + className=" text-center - border border-zinc-100"> - <td className="font-medium"> - {dataItem.rank ?? showSmartRank(dataItem, index, completes?.rows)} - </td> - <td className="font-semibold"> - {dataItem.value} - </td> - <td className="font-light"> - {truncate(dataItem.bpuid, 6, "*****").toUpperCase()} - </td> - </tr> - ))} - </tbody> - </table> - </div> + border border-zinc-100" + > + <td className="font-medium"> + {dataItem.rank ?? + showSmartRank(dataItem, index, completes?.rows)} + </td> + <td className="font-semibold">{dataItem.value}</td> + <td className="font-light"> + {truncate(dataItem.bpuid, 6, "*****").toUpperCase()} + </td> + </tr> + ))} + </tbody> + </table> + </div> + </div> + ); +}; + +type RequestState = "idle" | "working" | "failed"; + +interface LoginFormProps { + onSubmitSuccess: () => void; +} + +const LoginForm = function ({ onSubmitSuccess }: LoginFormProps) { + const [email, setEmail] = useState(""); + const [state, setState] = useState<RequestState>("idle"); + + // const dispatch = useAppDispatch(); + const submit = async (event: FormEvent<HTMLFormElement>) => { + event.preventDefault(); + setState("working"); + + try { + const response = await fetch("/auth/magic-link/request", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: email, + }), + }); + + if (!response.ok) throw new Error("Account-link request failed"); + const body = (await response.json()) as { magic_link?: string }; + console.log(body.magic_link ?? null); + onSubmitSuccess(); + } catch { + setState("failed"); + } + }; + + return ( + <> + <form className="w-full" onSubmit={submit}> + <div className="flex items-center border-b-4 border-blue-700 py-2"> + <input + className="appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none font-semibold" + type="email" + autoComplete="email" + required + value={email} + onChange={(event) => setEmail(event.target.value)} + placeholder="hello@jamesbillings67.com" + aria-label="Email address" + /> + + <button + className="flex-shrink-0 bg-blue-700 hover:bg-blue-800 text-white py-1 px-2 rounded cursor-pointer font-semibold" + type="submit" + disabled={state === "working"} + > + {state === "working" ? "Sending…" : "Login / Signup"} + </button> </div> - ); + </form> + + {state === "failed" && <p>Unable to send the link. Please try again.</p>} + </> + ); }; +const LoginSent = function () { + return ( + <div className="my-1"> + <h2 + className=" + bg-emerald-400 rounded py-2 + text-white text-l font-semibold text-center" + > + Please check your email. + </h2> + <p className="text-slate-800 text-xs font-semibold text-center my-1 py-2"> + If you don't recieve anything, please check your spam folder, or try + again shortly. + </p> + </div> + ); +}; +const Login = () => { + const [submitted, setSubmitted] = useState(false); + + return ( + <> + {submitted ? ( + <LoginSent /> + ) : ( + <LoginForm onSubmitSuccess={() => setSubmitted(true)} /> + )} + </> + ); +}; // Preview Component const Preview = () => { - /** - * - * @returns {JSX.Element} - */ - - const users = useAppSelector(state => activeUsers(state)) - const users_f: string = users?.toLocaleString() ?? " – " - - const surveys = useAppSelector(state => activeSurveys(state)) - const surveys_f: string = surveys?.toLocaleString() ?? " – " - - const survey_max_cpi = useAppSelector(state => maxPayout(state)) - const formatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - }); - const survey_max_cpi_f: string = formatter.format((survey_max_cpi ?? 0) / 100) - - return ( - <div className="max-h-[calc(100vh-10rem)] overflow-y-auto"> - - <div className="bg-zinc-100 rounded py-2 m-0"> - <p className="text-center text-sm italic text-zinc-600"> - {users_f} people are actively attempting {surveys_f} available surveys right now. - </p> - </div> - - <Leaderboards /> - - <div className="bg-zinc-100 rounded py-2 px-6 m-0 text-xs text-center"> - <p className="py-1"> - Get paid to take surveys. Our algorithm attempts to pair you with the - highest paying survey available at the moment (right now, the highest - paying survey is {survey_max_cpi_f}) that you are eligible for, - each attempt is a valid HIT. Try as many as you can, our system will - self regulate you: we want to ensure you have a high chance of being - paired with a survey based on what surveys are available, as their - availability constantly changes.</p> - <p className="py-1"> - Each HIT is paid when you attempt to take a survey and if you complete - the survey, you will be awarded a bonus equal to that in which the - survey pays out.</p> - </div> + /** + * + * @returns {JSX.Element} + */ + + const users = useAppSelector((state) => activeUsers(state)); + const users_f: string = users?.toLocaleString() ?? " – "; + + const surveys = useAppSelector((state) => activeSurveys(state)); + const surveys_f: string = surveys?.toLocaleString() ?? " – "; + + const survey_max_cpi = useAppSelector((state) => maxPayout(state)); + const formatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }); + const survey_max_cpi_f: string = formatter.format( + (survey_max_cpi ?? 0) / 100, + ); + + return ( + <> + <Login /> + + <div className="max-h-[calc(100vh-10rem)] overflow-y-auto"> + <div className="bg-zinc-100 rounded py-2 m-0"> + <p className="text-center text-sm italic text-zinc-600"> + {users_f} people are actively attempting {surveys_f} available + surveys right now. + </p> </div> - ); + + <Leaderboards /> + + <div className="bg-zinc-100 rounded py-2 px-6 m-0 text-xs text-center"> + <p className="py-1"> + Get paid to take surveys. Our algorithm attempts to pair you with + the highest paying survey available at the moment (right now, the + highest paying survey is {survey_max_cpi_f}) that you are eligible + for. Try as many as you can, our system will self regulate you: we + want to ensure you have a high chance of being paired with a survey + based on what surveys are available, as their availability + constantly changes. + </p> + </div> + </div> + </> + ); }; -export default Preview;
\ No newline at end of file +export default Preview; diff --git a/jb-ui/src/pages/Result.tsx b/jb-ui/src/pages/Result.tsx index 47af248..6385bc7 100644 --- a/jb-ui/src/pages/Result.tsx +++ b/jb-ui/src/pages/Result.tsx @@ -10,114 +10,116 @@ import { useEffect } from "react"; // } export const Complete = () => { - const ts = useAppSelector(state => state.app.taskStatus) - - return ( - <> - <h2 className="text-lg">Survey Completed</h2> - <p className="text-emerald-700 text-lg font-bold">Congratulations! You completed the Survey.</p> - <p className="text-emerald-700 text-2xl font-bold">{ts?.user_payout_string}</p> - <p className="text-slate-300">You may close this tab and return to submit the HIT</p> - </> - ) -} + const ts = useAppSelector((state) => state.app.taskStatus); + + return ( + <> + <h2 className="text-lg">Survey Completed</h2> + <p className="text-emerald-700 text-lg font-bold"> + Congratulations! You completed the Survey. + </p> + <p className="text-emerald-700 text-2xl font-bold"> + {ts?.user_payout_string} + </p> + <p className="text-slate-300">You may close this tab</p> + </> + ); +}; export const Enter = () => { - return ( - <> - <h2 className="text-lg">Entered</h2> - <p className="text-slate-800 text-lg font-bold">You're currently in the Survey</p> - <p className="text-slate-300">You may close this tab and return to submit the HIT</p> - </> - ) -} + return ( + <> + <h2 className="text-lg">Entered</h2> + <p className="text-slate-800 text-lg font-bold"> + You're currently in the Survey + </p> + <p className="text-slate-300">You may close this tab</p> + </> + ); +}; export const InComplete = () => { - const ts = useAppSelector(state => state.app.taskStatus) - if (!ts) { return null; } - - return ( - <> - <h2 className="text-lg">HIT Complete</h2> - <p className="text-rose-800 text-lg font-bold">Survey terminated the attempt.</p> - <p className="text-slate-300">You may close this tab and return to submit the HIT</p> - </> - - ) -} + const ts = useAppSelector((state) => state.app.taskStatus); + if (!ts) { + return null; + } + + return ( + <> + <h2 className="text-lg">Attempt Complete</h2> + <p className="text-rose-800 text-lg font-bold"> + Survey terminated the attempt. + </p> + <p className="text-slate-300">You may close this tab</p> + </> + ); +}; export const Unknown = () => { - - return ( - <> - <h2 className="text-lg">Task Issue</h2> - <p className="text-rose-800 text-lg font-bold">You're not currently eligible for any Surveys</p> - <p className="text-slate-300">You may close this tab and return to submit the HIT</p> - </> - ) -} + return ( + <> + <h2 className="text-lg">Task Issue</h2> + <p className="text-rose-800 text-lg font-bold"> + You're not currently eligible for any Surveys + </p> + <p className="text-slate-300">You may close this tab</p> + </> + ); +}; export const Loading = () => { - - return ( - <p className="text-2xl font-semibold">Loading...</p> - ) -} - + return <p className="text-2xl font-semibold">Loading...</p>; +}; const Result = () => { - - const ts = useAppSelector(state => state.app.taskStatus) - - const confettiCannons = () => { - const end = Date.now() + 3 * 1000 - const colors = ["#84cc16", "#1e3a8a", "#ec4899"] - const frame = () => { - if (Date.now() > end) return - confetti({ - particleCount: 3, - angle: 60, - spread: 55, - startVelocity: 80, - gravity: 0.7, - origin: { x: 0, y: 0.8 }, - colors: colors, - }) - confetti({ - particleCount: 3, - angle: 120, - spread: 55, - startVelocity: 80, - gravity: 0.7, - origin: { x: 1, y: 0.8 }, - colors: colors, - }) - requestAnimationFrame(frame) - } - frame() + const ts = useAppSelector((state) => state.app.taskStatus); + + const confettiCannons = () => { + const end = Date.now() + 3 * 1000; + const colors = ["#84cc16", "#1e3a8a", "#ec4899"]; + const frame = () => { + if (Date.now() > end) return; + confetti({ + particleCount: 3, + angle: 60, + spread: 55, + startVelocity: 80, + gravity: 0.7, + origin: { x: 0, y: 0.8 }, + colors: colors, + }); + confetti({ + particleCount: 3, + angle: 120, + spread: 55, + startVelocity: 80, + gravity: 0.7, + origin: { x: 1, y: 0.8 }, + colors: colors, + }); + requestAnimationFrame(frame); + }; + frame(); + }; + + useEffect(() => { + if (ts?.status === 3) { + confettiCannons(); } - - useEffect(() => { - if (ts?.status === 3) { - confettiCannons() - } - }, [ts]) - - - switch (ts?.status) { - case 0: // UNKNOWN - return <Unknown />; - case 1: // ENTER - return <Enter />; - case 2: // INCOMPLETE - return <InComplete />; - case 3: // COMPLETE - return <Complete />; - default: - return <Loading />; - } - + }, [ts]); + + switch (ts?.status) { + case 0: // UNKNOWN + return <Unknown />; + case 1: // ENTER + return <Enter />; + case 2: // INCOMPLETE + return <InComplete />; + case 3: // COMPLETE + return <Complete />; + default: + return <Loading />; + } }; - -export default Result;
\ No newline at end of file +export default Result; diff --git a/jb-ui/src/pages/Transfer.tsx b/jb-ui/src/pages/Transfer.tsx new file mode 100644 index 0000000..8079da2 --- /dev/null +++ b/jb-ui/src/pages/Transfer.tsx @@ -0,0 +1,162 @@ +import { type FormEvent, useState } from "react"; +import { useAppSelector } from "@/hooks"; +import { useGetMeQuery } from "@/models/authAPI"; +import { Button } from "@/components/ui/button"; +import max_lulu from "./max-and-lulu.jpg"; + +// const handleInputChange = ( +// event: React.KeyboardEvent<HTMLInputElement>, +// ) => {}; + +// const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => {}; +// const handleSubmit = (e) => { +// e.preventDefault(); +// console.log("Form submitted successfully!"); +// }; + +const TransferComplete = function () { + return ( + <div className="px-6 my-6"> + <h2 + className=" + bg-emerald-400 rounded py-2 + text-white text-2xl font-semibold text-center" + > + Please check your email. + </h2> + <p className="text-slate-600 text-l font-semibold text-center py-5 px-6"> + If you don't recieve anything, please check your spam folder, or try + again shortly. + </p> + <p className="bg-slate-200 text-xs font-semibold text-center p-3 my-3"> + We will be assisting account transfers throughout the rest of the month. + If you have any further questions or concerns, please email us directly + with your desired account email and the AMT ID to associate with your + worker history. + </p> + <img src={max_lulu} alt="Max and Lulu" className="object-contain" /> + </div> + ); +}; + +type RequestState = "idle" | "working" | "sent" | "failed"; + +interface TransferSubmitProps { + onSubmitSuccess: () => void; +} + +const TransferSubmit = function ({ onSubmitSuccess }: TransferSubmitProps) { + const [email, setEmail] = useState(""); + const [state, setState] = useState<RequestState>("idle"); + const { data: user } = useGetMeQuery(); + + const submit = async (event: FormEvent<HTMLFormElement>) => { + event.preventDefault(); + setState("working"); + + try { + const response = await fetch("/auth/link-amt/request", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: email, + amt_worker_id: user?.product_user_id, + }), + }); + + if (!response.ok) throw new Error("Account-link request failed"); + + const body = (await response.json()) as { magic_link?: string }; + console.log(body.magic_link ?? null); + onSubmitSuccess(); + } catch { + setState("failed"); + } + }; + + return ( + <div className="px-6 my-6"> + <h2 + className=" + bg-red-700 rounded py-2 + text-white text-2xl font-semibold text-center" + > + Transfer Required + </h2> + <h4 + className=" + mt-2 + bg-slate-500 rounded p-2 + text-white text-sm font-semibold text-center" + > + James Billings is no longer available on Amazon Mechanical Turk. Our + dedicated platform will enable you to do the same work as always, paid + directly to you. + <br /> + Enter your email below to transfer your account history. + <br /> + This will allow you to keep your full account history and wallet + balance. + </h4> + + <form onSubmit={submit}> + <input + className="w-full rounded-md border-3 border-slate-700 px-3 py-2 font-semibold my-5" + type="email" + autoComplete="email" + required + value={email} + onChange={(event) => setEmail(event.target.value)} + placeholder="you@example.com" + /> + <Button + className="w-1/2 font-semibold bg-slate-600 cursor-pointer hover:bg-blue-950" + type="submit" + disabled={state === "working"} + > + {state === "working" ? "Sending…" : "Send link"} + </Button> + </form> + + {state === "sent" && ( + <h4 + className=" + mt-2 + bg-slate-500 rounded py-2 + text-white text-l font-semibold text-center" + > + Check your email to finish linking your account. + </h4> + )} + + {state === "failed" && <p>Unable to send the link. Please try again.</p>} + </div> + ); +}; + +const Transfer = function () { + const bpuid = useAppSelector((state) => state.app.bpuid); + const [submitted, setSubmitted] = useState(false); + + if (!bpuid) { + return ( + <p className="text-center text-2xl py-3 text-red-800 font-semibold"> + No Worker ID found. Please access this page through the Amazon + Mechanical Turk platform. + </p> + ); + } + + return ( + <> + {submitted ? ( + <TransferComplete /> + ) : ( + <TransferSubmit onSubmitSuccess={() => setSubmitted(true)} /> + )} + </> + ); +}; + +export default Transfer; diff --git a/jb-ui/src/pages/Work.tsx b/jb-ui/src/pages/Work.tsx index c54732f..6d0b1c1 100644 --- a/jb-ui/src/pages/Work.tsx +++ b/jb-ui/src/pages/Work.tsx @@ -1,765 +1,818 @@ +import { useGetMeQuery } from "@/models/authAPI"; +import { Link } from "react-router-dom"; import { - BucketTask, - OfferwallApi, - OfferwallReason, - ReportApi, ReportTask, ReportValue, StatusApi, - TasksStatusResponse, TaskStatusResponse, TopNPlusBlockOfferWall + BucketTask, + OfferwallApi, + OfferwallReason, + ReportApi, + ReportTask, + ReportValue, + StatusApi, + TasksStatusResponse, + TaskStatusResponse, + TopNPlusBlockOfferWall, } from "@/api_fsb"; +import Preview from "@/pages/Preview"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { - Field, - FieldDescription, - FieldGroup, - FieldLabel, - FieldLegend, - FieldSet, + Field, + FieldDescription, + FieldGroup, + FieldLabel, + FieldLegend, + FieldSet, } from "@/components/ui/field"; import { useAppDispatch, useAppSelector } from "@/hooks"; import { bpid, formatCentsToUSD, formatSource, truncate } from "@/lib/utils"; import { - getAvailabilityCount, getLOIText, - isLowBalance, - selectBucket, - setAttemptedLiveEligibleCount, - setAvailabilityCount, - setCurrentBuckets, - setEnteredTimestamp, setLOI, - setOfferwallReasons, - setTaskStatus + getAvailabilityCount, + getLOIText, + isLowBalance, + selectBucket, + setAttemptedLiveEligibleCount, + setAvailabilityCount, + setCurrentBuckets, + setEnteredTimestamp, + setLOI, + setOfferwallReasons, + setTaskStatus, } from "@/models/appSlice"; import { - createColumnHelper, flexRender, getCoreRowModel, - useReactTable + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, } from "@tanstack/react-table"; import { XIcon } from "lucide-react"; import moment from "moment/moment"; -import { useEffect, useRef, useState } from "react"; -import Markdown from 'react-markdown'; +import { useEffect, useState } from "react"; +import Markdown from "react-markdown"; const Report = ({ onClose }: { onClose: () => void }) => { - const [showComplete, setShowComplete] = useState(false); - const [selectedValues, setSelectedValues] = useState<ReportValue[]>([]); - - const bpuid = useAppSelector(state => state.app.bpuid) - const ts = useAppSelector(state => state.app.taskStatus) - - const handleCheckboxChange = (value: ReportValue, checked: boolean) => { - setSelectedValues(prev => - checked - ? [...prev, value] - : prev.filter(v => v !== value) - ); + const [, setShowComplete] = useState(false); + const [selectedValues, setSelectedValues] = useState<ReportValue[]>([]); + const { data: user } = useGetMeQuery(); + const ts = useAppSelector((state) => state.app.taskStatus); + + const handleCheckboxChange = (value: ReportValue, checked: boolean) => { + setSelectedValues((prev) => + checked ? [...prev, value] : prev.filter((v) => v !== value), + ); + }; + + const clicked_submit = () => { + if (!user?.product_user_id) return; + // ReportValue.NUMBER_0 + console.log("selectedValues:", selectedValues); + + const rt: ReportTask = { + bpuid: user.product_user_id, + reasons: selectedValues, + notes: "", }; - const clicked_submit = () => { - if (!bpuid) return; - // ReportValue.NUMBER_0 - console.log("selectedValues:", selectedValues); - - const rt: ReportTask = { - 'bpuid': bpuid, - 'reasons': selectedValues, - 'notes': "" - }; - - new ReportApi().reportTaskProductIdReportPost( - bpid, // productId - rt, // reportTask - ).then(res => { - console.log("Response:", res); // Check what's actually here - - if (res.status === 400) { - // Handle error here instead - } else { - setShowComplete(true); - } - }) - } - - const reportReasons = [ - { id: 'TECHNICAL_ERROR', label: 'Technical Error', value: ReportValue.NUMBER_1 }, - { id: 'NO_REDIRECT', label: 'No Redirect', value: ReportValue.NUMBER_2 }, - { id: 'PRIVACY_INVASION', label: 'Privacy Invasion', value: ReportValue.NUMBER_3 }, - { id: 'UNCOMFORTABLE_TOPICS', label: 'Uncomfortable Topics', value: ReportValue.NUMBER_4 }, - { id: 'ASKED_FOR_NOT_ALLOWED_ACTION', label: 'Asked for Not Allowed Action', value: ReportValue.NUMBER_5 }, - { id: 'BAD_ON_MOBILE', label: 'Bad on Mobile', value: ReportValue.NUMBER_6 }, - { id: 'DIDNT_LIKE', label: "Didn't Like", value: ReportValue.NUMBER_7 }, - ]; - - if (!ts?.finished) { - return <p>Please finish a Task to report its status.</p> - } - - if (showComplete) { - return <SubimtAMT /> - } - - return ( - <div className="border-2 border-rose-200 rounded"> - <div className="flex justify-center"> - <FieldSet className="mt-2"> - <FieldLegend variant="label" className="my-0 text-rose-950"> - Please select all reasons why you are reporting this Task: - </FieldLegend> - <FieldDescription className="my-0 text-rose-950"> - You must provide at least one reason to report a Task. - </FieldDescription> - <FieldGroup className="gap-2 my-0 py-0"> - - {reportReasons.map((reason) => ( - <Field key={reason.id} orientation="horizontal" className=" - hover:cursor-pointer"> - <Checkbox - id={reason.id} - name={reason.id} - onCheckedChange={(checked) => - handleCheckboxChange(reason.value, checked as boolean) - } - /> - <FieldLabel - htmlFor={reason.id} - className="font-medium text-rose-950 + new ReportApi() + .reportTaskProductIdReportPost( + bpid, // productId + rt, // reportTask + ) + .then((res) => { + console.log("Response:", res); // Check what's actually here + + if (res.status === 400) { + // Handle error here instead + } else { + setShowComplete(true); + } + }); + }; + + const reportReasons = [ + { + id: "TECHNICAL_ERROR", + label: "Technical Error", + value: ReportValue.NUMBER_1, + }, + { id: "NO_REDIRECT", label: "No Redirect", value: ReportValue.NUMBER_2 }, + { + id: "PRIVACY_INVASION", + label: "Privacy Invasion", + value: ReportValue.NUMBER_3, + }, + { + id: "UNCOMFORTABLE_TOPICS", + label: "Uncomfortable Topics", + value: ReportValue.NUMBER_4, + }, + { + id: "ASKED_FOR_NOT_ALLOWED_ACTION", + label: "Asked for Not Allowed Action", + value: ReportValue.NUMBER_5, + }, + { + id: "BAD_ON_MOBILE", + label: "Bad on Mobile", + value: ReportValue.NUMBER_6, + }, + { id: "DIDNT_LIKE", label: "Didn't Like", value: ReportValue.NUMBER_7 }, + ]; + + if (!ts?.finished) { + return <p>Please finish a Task to report its status.</p>; + } + + // TODO! What do we do here, redirect back to / with a thank you + wallet refresh? + // if (showComplete) { + // return <SubimtAMT />; + // } + + return ( + <div className="border-2 border-rose-200 rounded"> + <div className="flex justify-center"> + <FieldSet className="mt-2"> + <FieldLegend variant="label" className="my-0 text-rose-950"> + Please select all reasons why you are reporting this Task: + </FieldLegend> + <FieldDescription className="my-0 text-rose-950"> + You must provide at least one reason to report a Task. + </FieldDescription> + <FieldGroup className="gap-2 my-0 py-0"> + {reportReasons.map((reason) => ( + <Field + key={reason.id} + orientation="horizontal" + className=" + hover:cursor-pointer" + > + <Checkbox + id={reason.id} + name={reason.id} + onCheckedChange={(checked) => + handleCheckboxChange(reason.value, checked as boolean) + } + /> + <FieldLabel + htmlFor={reason.id} + className="font-medium text-rose-950 hover:cursor-pointer hover:font-semibold" - > - {reason.label} - </FieldLabel> - </Field> - ))} - - </FieldGroup> - </FieldSet> - </div> - <div className="flex justify-center my-2 space-x-5"> - <Button variant="outline" size="icon" aria-label="Submit" - className=" + > + {reason.label} + </FieldLabel> + </Field> + ))} + </FieldGroup> + </FieldSet> + </div> + <div className="flex justify-center my-2 space-x-5"> + <Button + variant="outline" + size="icon" + aria-label="Submit" + className=" border-2 rounded border-zinc-800 text-zinc-800 hover:cursor-pointer hover:bg-zinc-800 hover:text-zinc-50" - onClick={onClose}> - <XIcon /> - </Button> - <Button variant="outline" - className=" + onClick={onClose} + > + <XIcon /> + </Button> + <Button + variant="outline" + className=" border-2 rounded border-rose-400 text-rose-950 hover:cursor-pointer hover:bg-rose-400 hover:text-rose-50" - onClick={clicked_submit}>Submit and Complete HIT</Button> - </div> - </div> - ) -} + onClick={clicked_submit} + > + Submit and Complete Attempt + </Button> + </div> + </div> + ); +}; -const SubimtAMT = () => { - const bpuid = useAppSelector(state => state.app.bpuid) - const assignment_id = useAppSelector(state => state.app.assignment_id) - const ts = useAppSelector(state => state.app.taskStatus) - const turkSubmitTo = useAppSelector(state => state.app.turkSubmitTo) +const SurveyMonitor = () => { + const dispatch = useAppDispatch(); - const formRef = useRef<HTMLFormElement>(null); + const [isPolling, setIsPolling] = useState(true); + // const [timeAgo, setTimeAgo] = useState(""); - useEffect(() => { - if (!ts) return; - if (!bpuid) return; - if (!assignment_id) return; - if (!turkSubmitTo) return; + const [, setShowComplete] = useState(false); + const [showReport, setShowReport] = useState(false); - if (formRef.current) { - formRef.current.submit(); - } - }, [ts?.tsid, bpuid, assignment_id, turkSubmitTo]); + const { data: user } = useGetMeQuery(); + const entered = useAppSelector((state) => state.app.currentBucketEntered); + const isLow = useAppSelector((state) => isLowBalance(state)); - return ( - <> - <form - ref={formRef} - action={`${turkSubmitTo}/mturk/externalSubmit`} - method="POST" - style={{ display: 'none' }}> - <input type="hidden" name="assignmentId" value={assignment_id} /> - <input type="hidden" name="tsid" value={ts?.tsid} /> - <input type="hidden" name="amt_assignment_id" value={assignment_id} /> - <input type="hidden" name="amt_worker_id" value={bpuid} /> - <button type="submit">Submit HIT</button> - </form> - </> - ) -} + const ts = useAppSelector((state) => state.app.taskStatus); + const isBaddie = + ts?.status_code_1 === "SESSION_START_FAIL" || + ts?.status_code_1 === "SESSION_START_QUALITY_FAIL" || + ts?.status_code_1 === "SESSION_CONTINUE_QUALITY_FAIL"; + // useEffect(() => { + // if (!ts) return; -const SurveyMonitor = () => { - const dispatch = useAppDispatch() - - const [isPolling, setIsPolling] = useState(true); - // const [timeAgo, setTimeAgo] = useState(""); - - const [showComplete, setShowComplete] = useState(false); - const [showReport, setShowReport] = useState(false); - - const bpuid = useAppSelector(state => state.app.bpuid) - const entered = useAppSelector(state => state.app.currentBucketEntered) - const isLow = useAppSelector(state => isLowBalance(state)) - - const ts = useAppSelector(state => state.app.taskStatus) - const isBaddie = ts?.status_code_1 === "SESSION_START_FAIL" || ts?.status_code_1 === "SESSION_START_QUALITY_FAIL" || ts?.status_code_1 === "SESSION_CONTINUE_QUALITY_FAIL"; - - // useEffect(() => { - // if (!ts) return; - - // const updateTimeAgo = () => { - // const time_ago = moment(ts.started).fromNow(); - // setTimeAgo(time_ago); - // }; - - // // Update immediately - // updateTimeAgo(); - - // // Then update every minute - // const intervalId = setInterval(updateTimeAgo, 60 * 1000); - - // return () => clearInterval(intervalId); - // }, [ts]); - - useEffect(() => { - if (!bpuid) return; - if (!entered) return; - if (!isPolling) return; - - const pollApi = async () => { - new StatusApi().listTaskStatusesProductIdStatusGet( - bpid, // productId - bpuid, // Worker ID - entered, // startedAfter - moment.utc().unix() // startedBefore - ).then(res => { - const d = res.data as TasksStatusResponse; - - if (!d.tasks_status || d.tasks_status.length === 0) { - // Likely still in a GRS survey - return; - } - - // if (d_len >= 2) Sentry.captureMessage("Results returned back multiple") - const ts = d.tasks_status![0] as TaskStatusResponse; - dispatch(setTaskStatus(ts)) - - if (ts.status == 2 || ts.status == 3) { - // INCOMPLETE + COMPLETE - setIsPolling(false); - } - }); - - // If there is already a TaskStatus, update the time ago - // if (ts) { - // const time_ago = moment(ts.started).fromNow(); - // setTimeAgo(time_ago); - // } - }; - - // Call immediately on mount, then set up interval - pollApi(); - const intervalId = setInterval(pollApi, 4 * 1000); - - // Cleanup: stop interval when component unmounts or isPolling changes - return () => clearInterval(intervalId); - }, [isPolling, entered]); - - if (!ts) { - return ( - <p>Loading</p> - ) - } - - // This is if they are actively in a survey or we can't determine the - // status yet. We want to show the timer and keep them on this page to - // monitor for completion. This gets shown no matter what regardless of - // balance. - // - // 0 - UNKNOWN, 1 - ENTER - if (ts.status === 0 || ts.status === 1) { - return ( - <> - <div> - <h2 className=" - bg-blue-500 rounded py-2 - text-white text-2xl font-semibold text-center"> - In Progress - </h2> - <p className=" - py-4 - text-zinc-500 text-2xl font-semibold text-center"> - We're actively monitoring your Task status. - </p> - </div> - </> + // const updateTimeAgo = () => { + // const time_ago = moment(ts.started).fromNow(); + // setTimeAgo(time_ago); + // }; + + // // Update immediately + // updateTimeAgo(); + + // // Then update every minute + // const intervalId = setInterval(updateTimeAgo, 60 * 1000); + + // return () => clearInterval(intervalId); + // }, [ts]); + + useEffect(() => { + if (!user?.product_user_id) return; + if (!entered) return; + if (!isPolling) return; + + const pollApi = async () => { + new StatusApi() + .listTaskStatusesProductIdStatusGet( + bpid, // productId + user.product_user_id, // Worker ID + entered, // startedAfter + moment.utc().unix(), // startedBefore ) - } + .then((res) => { + const d = res.data as TasksStatusResponse; + + if (!d.tasks_status || d.tasks_status.length === 0) { + // Likely still in a GRS survey + return; + } + + // if (d_len >= 2) Sentry.captureMessage("Results returned back multiple") + const ts = d.tasks_status![0] as TaskStatusResponse; + dispatch(setTaskStatus(ts)); + + if (ts.status == 2 || ts.status == 3) { + // INCOMPLETE + COMPLETE + setIsPolling(false); + } + }); - // This is if they have finished a survey complete and it resulted in a - // COMPLETE or a INCOMPLETE (doesn't matter). They can now submit the HIT. - // - // 2 - INCOMPLETE, 3 - COMPLETE + // If there is already a TaskStatus, update the time ago + // if (ts) { + // const time_ago = moment(ts.started).fromNow(); + // setTimeAgo(time_ago); + // } + }; + + // Call immediately on mount, then set up interval + pollApi(); + const intervalId = setInterval(pollApi, 4 * 1000); + + // Cleanup: stop interval when component unmounts or isPolling changes + return () => clearInterval(intervalId); + }, [isPolling, entered]); + + if (!ts) { + return <p>Loading</p>; + } + + // This is if they are actively in a survey or we can't determine the + // status yet. We want to show the timer and keep them on this page to + // monitor for completion. This gets shown no matter what regardless of + // balance. + // + // 0 - UNKNOWN, 1 - ENTER + if (ts.status === 0 || ts.status === 1) { return ( - <> - <div> - <h2 className=" + <> + <div> + <h2 + className=" bg-blue-500 rounded py-2 - text-white text-2xl font-semibold text-center"> - {ts.status === 3 ? "Survey Complete!" : "Survey Terminated."} - </h2> - - {((ts.status === 3 || !isLow) && !isBaddie) && ( - <p className=" + text-white text-2xl font-semibold text-center" + > + In Progress + </h2> + <p + className=" + py-4 + text-zinc-500 text-2xl font-semibold text-center" + > + We're actively monitoring your Task status. + </p> + </div> + </> + ); + } + + // This is if they have finished a survey complete and it resulted in a + // COMPLETE or a INCOMPLETE (doesn't matter). They can now submit the Attempt. + // + // 2 - INCOMPLETE, 3 - COMPLETE + return ( + <> + <div> + <h2 + className=" + bg-blue-500 rounded py-2 + text-white text-2xl font-semibold text-center" + > + {ts.status === 3 ? "Survey Complete!" : "Survey Terminated."} + </h2> + + {(ts.status === 3 || !isLow) && !isBaddie && ( + <p + className=" py-2 - text-zinc-500 text-md font-medium text-center"> - You may now submit the HIT. - </p> - )} - </div> - - {(isBaddie) ? ( - <> - <div className=" + text-zinc-500 text-md font-medium text-center" + > + You may now submit the attempt. + </p> + )} + </div> + + {isBaddie ? ( + <> + <div + className=" border-2 border-rose-500 rounded py-2 - bg-rose-400 text-rose-700"> - <h2 className="text-center text-xl font-bold text-white"> - Quality Check Failed. - </h2> - <p className="px-4 py-2 text-center text-sm font-medium text-white"> - Unfortunately, it looks like you did not pass the quality check for this survey. - </p> - - <p className=" + bg-rose-400 text-rose-700" + > + <h2 className="text-center text-xl font-bold text-white"> + Quality Check Failed. + </h2> + <p className="px-4 py-2 text-center text-sm font-medium text-white"> + Unfortunately, it looks like you did not pass the quality check + for this survey. + </p> + + <p + className=" pt-0 pb-4 - text-red-950 text-sm font-semibold text-center"> - Please return the HIT to avoid having your assignment rejected. - </p> - </div> - </> - - ) : ( - <> - {(ts.status === 3 || !isLow) ? ( - <> - <div className="flex justify-center my-4 space-x-5"> - - <button type="button" className=" + text-red-950 text-sm font-semibold text-center" + > + <Link to="/" reloadDocument> + Click to Refresh and Try Again + </Link> + </p> + </div> + </> + ) : ( + <> + {ts.status === 3 || !isLow ? ( + <> + <div className="flex justify-center my-4 space-x-5"> + <button + type="button" + className=" border-4 rounded border-emerald-400 py-1 px-3 text-xl font-bold text-emerald-950 hover:cursor-pointer hover:bg-emerald-400 hover:text-emerald-50" - onClick={() => setShowComplete(true)} - >Submit HIT</button> - - <button type="button" className=" + onClick={() => setShowComplete(true)} + > + Finish Attempt + </button> + + <button + type="button" + className=" border-4 rounded border-rose-200 py-1 px-3 text-xl font-bold text-rose-200 hover:cursor-pointer hover:border-rose-500 hover:bg-rose-400 hover:text-rose-50" - onClick={() => setShowReport(!showReport)} - >Report Survey</button> - </div> - - {showComplete && <SubimtAMT />} - {showReport && <Report onClose={() => setShowReport(false)} />} - </> - ) : ( - <div> - <p className=" - text-red-600 text-lg font-semibold text-center"> - Your balance is low. You cannot submit the HIT at this time. - </p> - - <p className=" - pt-0 pb-4 - text-red-950 text-sm font-semibold text-center"> - Please return the HIT to avoid having your assignment rejected. - </p> - </div> - )} - </> - - )} - - + onClick={() => setShowReport(!showReport)} + > + Report Survey + </button> + </div> + {/* TODO! What do we do here, redirect back to / with wallet */} + {/* TODO! refresh? The confetti is on a different page. */} + {/* {showComplete && <SubimtAMT />} */} + {showReport && <Report onClose={() => setShowReport(false)} />} + </> + ) : ( + <div> + <p + className=" + text-red-600 text-lg font-semibold text-center" + > + Your balance is low. + </p> + </div> + )} </> - ) -} - + )} + </> + ); +}; const LOISelect = () => { - // This is what's responsible for being able to click on the text - // we set the global LOI times. - - const dispatch = useAppDispatch() - const loi: number = useAppSelector(state => state.app.loi) - const bucket = useAppSelector(state => selectBucket(state)) - - const toggle_loi = function () { - dispatch(setLOI(loi >= 1800 ? 600 : loi + 600)) - } - - const time_f = useAppSelector(state => getLOIText(state)) - - const avg_time_f = ((bucket?.duration.q2 ?? 0) / 60).toLocaleString( - 'en-US', { minimumFractionDigits: 0, maximumFractionDigits: 1 }) - - const formatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - }); - const avg_pay_f: string = formatter.format((bucket?.payout.q2 ?? 0) / 100) - - return ( - <div className="bg-zinc-100 rounded py-2 m-0"> - <p className="text-zinc-700 text-2xl text-center font-bold m-0"> - I have <span - className="cursor-pointer text-emerald-600 italic hover:text-emerald-700 hover:underline" - onClick={toggle_loi}>{time_f}</span> minutes - </p> - <p - className="text-center text-xs font-thin m-0 text-zinc-500"> - The average time spent is {avg_time_f} minutes and pays {avg_pay_f}</p> - </div> - ) - + // This is what's responsible for being able to click on the text + // we set the global LOI times. + + const dispatch = useAppDispatch(); + const loi: number = useAppSelector((state) => state.app.loi); + const bucket = useAppSelector((state) => selectBucket(state)); + + const toggle_loi = function () { + dispatch(setLOI(loi >= 1800 ? 600 : loi + 600)); + }; + + const time_f = useAppSelector((state) => getLOIText(state)); + + const avg_time_f = ((bucket?.duration.q2 ?? 0) / 60).toLocaleString("en-US", { + minimumFractionDigits: 0, + maximumFractionDigits: 1, + }); + + const formatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }); + const avg_pay_f: string = formatter.format((bucket?.payout.q2 ?? 0) / 100); + + return ( + <div className="bg-zinc-100 rounded py-2 m-0"> + <p className="text-zinc-700 text-2xl text-center font-bold m-0"> + I have{" "} + <span + className="cursor-pointer text-emerald-600 italic hover:text-emerald-700 hover:underline" + onClick={toggle_loi} + > + {time_f} + </span>{" "} + minutes + </p> + <p className="text-center text-xs font-thin m-0 text-zinc-500"> + The average time spent is {avg_time_f} minutes and pays {avg_pay_f} + </p> + </div> + ); }; const SurveyLoading = () => { - return ( - <div className=" + return ( + <div + className=" border-8 border-emerald-400 rounded py-0 my-2 text-center text-3xl - hover:cursor-pointer hover:bg-green-500 hover:text-white"> - <p className="py-2 text-center text-3xl font-semibold"> - Survey Loading… - </p> - </div> - ) -} - + hover:cursor-pointer hover:bg-green-500 hover:text-white" + > + <p className="py-2 text-center text-3xl font-semibold">Survey Loading…</p> + </div> + ); +}; const NoSurveys = () => { - const offerwall_reasons = useAppSelector(state => state.app.offerwall_reasons) - - const REASON_DISPLAY: Record<OfferwallReason, { label: string; description: string }> = { - [OfferwallReason.UserBlocked]: { - label: "Account Restricted", - description: "Your account has been restricted.", - }, - [OfferwallReason.HighReconRate]: { - label: "High Reconciliation Rate", - description: "Your account has an unusually high reconciliation rate.", - }, - [OfferwallReason.UncommonDemographics]: { - label: "Demographic Mismatch", - description: "No surveys require your current demographics.", - }, - [OfferwallReason.UnderMinimumAge]: { - label: "Age Requirement Not Met", - description: "You must meet the minimum age requirement to participate.", - }, - [OfferwallReason.ExhaustedHighValueSupply]: { - label: "No Premium Offers Available", - description: "You've attempted all premium surveys.", - }, - [OfferwallReason.AllEligibleAttempted]: { - label: "All Offers Attempted", - description: "You've attempted all available surveys.", - }, - [OfferwallReason.LowCurrentSupply]: { - label: "Limited Availability", - description: "There are currently limited opportunities available.", - }, - }; + const offerwall_reasons = useAppSelector( + (state) => state.app.offerwall_reasons, + ); - function formatReasons(reasons: OfferwallReason[]): { label: string; description: string }[] { - return reasons.map((r) => REASON_DISPLAY[r]); - } - - return ( - <> - <div className=" + const REASON_DISPLAY: Record< + OfferwallReason, + { label: string; description: string } + > = { + [OfferwallReason.UserBlocked]: { + label: "Account Restricted", + description: "Your account has been restricted.", + }, + [OfferwallReason.HighReconRate]: { + label: "High Reconciliation Rate", + description: "Your account has an unusually high reconciliation rate.", + }, + [OfferwallReason.UncommonDemographics]: { + label: "Demographic Mismatch", + description: "No surveys require your current demographics.", + }, + [OfferwallReason.UnderMinimumAge]: { + label: "Age Requirement Not Met", + description: "You must meet the minimum age requirement to participate.", + }, + [OfferwallReason.ExhaustedHighValueSupply]: { + label: "No Premium Offers Available", + description: "You've attempted all premium surveys.", + }, + [OfferwallReason.AllEligibleAttempted]: { + label: "All Offers Attempted", + description: "You've attempted all available surveys.", + }, + [OfferwallReason.LowCurrentSupply]: { + label: "Limited Availability", + description: "There are currently limited opportunities available.", + }, + }; + + function formatReasons( + reasons: OfferwallReason[], + ): { label: string; description: string }[] { + return reasons.map((r) => REASON_DISPLAY[r]); + } + + return ( + <> + <div + className=" border-8 border-emerald-400 rounded py-0 my-2 text-center text-3xl - hover:cursor-pointer hover:bg-green-500 hover:text-white"> - <p className="py-2 text-center text-3xl font-semibold"> - No Surveys available… - </p> - </div> - - {formatReasons(offerwall_reasons).map(r => { - return ( - <div className=" + hover:cursor-pointer hover:bg-green-500 hover:text-white" + > + <p className="py-2 text-center text-3xl font-semibold"> + No Surveys available… + </p> + </div> + + {formatReasons(offerwall_reasons).map((r) => { + return ( + <div + className=" border-6 border-rose-200 rounded py-0 my-2 text-center - hover:cursor-pointer hover:bg-rose-200 hover:text-rose-950 "> - <p className="py-2 text-center text-md font-semibold text-rose-600"> - {r.description} - </p> - </div> - ) - })} - </> - ) -} + hover:cursor-pointer hover:bg-rose-200 hover:text-rose-950 " + > + <p className="py-2 text-center text-md font-semibold text-rose-600"> + {r.description} + </p> + </div> + ); + })} + </> + ); +}; const BucketDetails = () => { - const bucket = useAppSelector(state => selectBucket(state)) - if (!bucket) return null; - const [showDetails, setShowDetails] = useState(false); - - const columnHelper = createColumnHelper<BucketTask>() - const columns = [ - columnHelper.display({ - id: 'index', - header: '#', - cell: ({ row }) => row.index + 1, - size: 25, - meta: { - align: 'center' - } - }), - columnHelper.accessor('source', { - header: () => 'Marketplace', - cell: (props) => { - const s = props.getValue() - return formatSource(s) - }, - size: 85, - meta: { - align: 'left' - } - }), - columnHelper.accessor('id', { - header: () => 'Survey ID', - cell: props => { - return truncate(props.getValue(), 6, "*****").toUpperCase() - }, - size: 70, - meta: { - align: 'left' - } - }), - columnHelper.accessor('payout', { - header: () => 'Amount', - cell: (props) => { - const val = props.renderValue() as number; - return formatCentsToUSD(val) - }, - size: 50, - meta: { - align: 'center' - } - }), - columnHelper.accessor('loi', { - header: () => 'Duration', - cell: (props) => { - const val = props.renderValue() as number; - return `${Math.round(val / 60)} min` - }, - size: 50, - meta: { - align: 'center' - } - }), - ] - - const table = useReactTable({ - 'data': bucket.contents, - 'columns': columns, - getCoreRowModel: getCoreRowModel(), - }); - - return ( - <> - <div className="w-full text-center text-[8px] text-emerald-600 + const bucket = useAppSelector((state) => selectBucket(state)); + if (!bucket) return null; + const [showDetails, setShowDetails] = useState(false); + + const columnHelper = createColumnHelper<BucketTask>(); + const columns = [ + columnHelper.display({ + id: "index", + header: "#", + cell: ({ row }) => row.index + 1, + size: 25, + meta: { + align: "center", + }, + }), + columnHelper.accessor("source", { + header: () => "Marketplace", + cell: (props) => { + const s = props.getValue(); + return formatSource(s); + }, + size: 85, + meta: { + align: "left", + }, + }), + columnHelper.accessor("id", { + header: () => "Survey ID", + cell: (props) => { + return truncate(props.getValue(), 6, "*****").toUpperCase(); + }, + size: 70, + meta: { + align: "left", + }, + }), + columnHelper.accessor("payout", { + header: () => "Amount", + cell: (props) => { + const val = props.renderValue() as number; + return formatCentsToUSD(val); + }, + size: 50, + meta: { + align: "center", + }, + }), + columnHelper.accessor("loi", { + header: () => "Duration", + cell: (props) => { + const val = props.renderValue() as number; + return `${Math.round(val / 60)} min`; + }, + size: 50, + meta: { + align: "center", + }, + }), + ]; + + const table = useReactTable({ + data: bucket.contents, + columns: columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( + <> + <div + className="w-full text-center text-[8px] text-emerald-600 rounded py-2 my-1 hover:cursor-pointer hover:text-emerald-700 hover:underline hover:bg-emerald-200 " - onClick={() => setShowDetails(!showDetails)} - > - ({showDetails ? 'Hide' : 'Show'} details of surveys in this bucket) - </div> - - {showDetails && ( - <div className="w-full border-t-1 border-emerald-200 py-1 my-0 flex justify-center"> - <table className="text-[10px] text-emerald-900 table-fixed border-collapse"> - <thead className="font-bold"> - {table.getHeaderGroups().map((headerGroup) => ( - - <tr key={headerGroup.id}> - {headerGroup.headers.map((header) => { - const align = header.column.columnDef.meta?.align || 'left'; - const alignClass = align === 'center' ? 'text-center' : align === 'right' ? 'text-right' : 'text-left'; - - return ( - <th - key={header.id} - className={`p-0 m-0 ${alignClass}`} - style={{ - width: `${header.column.getSize()}px`, - }} - > - {flexRender( - header.column.columnDef.header, - header.getContext(), - )} - </th> - ) - })} - </tr> - ))} - </thead> - <tbody> - {table.getRowModel().rows.map((row) => ( - <tr key={row.id}> - {row.getVisibleCells().map((cell) => { - const align = cell.column.columnDef.meta?.align || 'left'; - const alignClass = align === 'center' ? 'text-center' : - align === 'right' ? 'text-right' : 'text-left'; - - return ( - <td key={cell.id} - className={`p-0 ${alignClass}`} - style={{ - width: `${cell.column.getSize()}px`, - }} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - </td> - ) - })} - </tr> - ))} - </tbody> - - </table> - </div> - )} - </> - ) + onClick={() => setShowDetails(!showDetails)} + > + ({showDetails ? "Hide" : "Show"} details of surveys in this bucket) + </div> + + {showDetails && ( + <div className="w-full border-t-1 border-emerald-200 py-1 my-0 flex justify-center"> + <table className="text-[10px] text-emerald-900 table-fixed border-collapse"> + <thead className="font-bold"> + {table.getHeaderGroups().map((headerGroup) => ( + <tr key={headerGroup.id}> + {headerGroup.headers.map((header) => { + const align = header.column.columnDef.meta?.align || "left"; + const alignClass = + align === "center" + ? "text-center" + : align === "right" + ? "text-right" + : "text-left"; + + return ( + <th + key={header.id} + className={`p-0 m-0 ${alignClass}`} + style={{ + width: `${header.column.getSize()}px`, + }} + > + {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + </th> + ); + })} + </tr> + ))} + </thead> + <tbody> + {table.getRowModel().rows.map((row) => ( + <tr key={row.id}> + {row.getVisibleCells().map((cell) => { + const align = cell.column.columnDef.meta?.align || "left"; + const alignClass = + align === "center" + ? "text-center" + : align === "right" + ? "text-right" + : "text-left"; + + return ( + <td + key={cell.id} + className={`p-0 ${alignClass}`} + style={{ + width: `${cell.column.getSize()}px`, + }} + > + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + </td> + ); + })} + </tr> + ))} + </tbody> + </table> + </div> + )} + </> + ); }; const SurveyEnter = () => { - const dispatch = useAppDispatch() - const loi = useAppSelector(state => state.app.loi) - const bpuid = useAppSelector(state => state.app.bpuid) - - const available_cnt = useAppSelector(state => getAvailabilityCount(state)) - const buckets = useAppSelector(state => state.app.currentBuckets) - const bucket = useAppSelector(state => selectBucket(state)) - const isLow = useAppSelector(state => isLowBalance(state)) - - useEffect(() => { - if (!bpuid) return; - - new OfferwallApi().topNPlusBlockOfferwallProductIdOfferwallD48cce47Get( - bpid, // productId - bpuid, // bpuid - undefined, // ip - undefined, // countryIso - undefined, // languages - undefined, // behavior - undefined, // minPayout - loi, //duration - 1 // nBins - ).then(res => { - const top_n_res = res.data.offerwall as TopNPlusBlockOfferWall; - dispatch(setAvailabilityCount(top_n_res.availability_count)) - if (top_n_res.attempted_live_eligible_count) { - dispatch(setAttemptedLiveEligibleCount(top_n_res.attempted_live_eligible_count)) - } - dispatch(setCurrentBuckets(top_n_res.buckets)) - dispatch(setOfferwallReasons(top_n_res.offerwall_reasons ?? [])) - }); - }, [loi, bpuid]); - - - const start_survey = () => { - if (!bucket?.uri) return; - - dispatch(setEnteredTimestamp()); - window.open(bucket.uri, '_blank'); - } - - if (buckets === undefined) { - return <SurveyLoading /> - } - - if (available_cnt === 0 || buckets.length === 0 || !bucket) { - return <NoSurveys /> - } - - return ( - <> - {isLow && ( - <div className=" + const dispatch = useAppDispatch(); + const loi = useAppSelector((state) => state.app.loi); + const { data: user } = useGetMeQuery(); + + const available_cnt = useAppSelector((state) => getAvailabilityCount(state)); + const buckets = useAppSelector((state) => state.app.currentBuckets); + const bucket = useAppSelector((state) => selectBucket(state)); + const isLow = useAppSelector((state) => isLowBalance(state)); + + useEffect(() => { + if (!user?.product_user_id) return; + + new OfferwallApi() + .topNPlusBlockOfferwallProductIdOfferwallD48cce47Get( + bpid, // productId + // "7d9240132c695c05cd0ba47e2580158719f8215b", + // "71.225.51.26", + user.product_user_id, + undefined, // ip + undefined, // countryIso + undefined, // languages + undefined, // behavior + undefined, // minPayout + loi, //duration + 1, // nBins + ) + .then((res) => { + const top_n_res = res.data.offerwall as TopNPlusBlockOfferWall; + dispatch(setAvailabilityCount(top_n_res.availability_count)); + if (top_n_res.attempted_live_eligible_count) { + dispatch( + setAttemptedLiveEligibleCount( + top_n_res.attempted_live_eligible_count, + ), + ); + } + dispatch(setCurrentBuckets(top_n_res.buckets)); + dispatch(setOfferwallReasons(top_n_res.offerwall_reasons ?? [])); + }); + }, [loi, user?.product_user_id]); + + const start_survey = () => { + if (!bucket?.uri) return; + + dispatch(setEnteredTimestamp()); + window.open(bucket.uri, "_blank"); + }; + + if (buckets === undefined) { + return <SurveyLoading />; + } + + if (available_cnt === 0 || buckets.length === 0 || !bucket) { + return <NoSurveys />; + } + + return ( + <> + {isLow && ( + <div + className=" border-8 border-rose-500 rounded py-2 - bg-rose-400 text-rose-700"> - <h2 className="text-center text-xl font-bold text-white"> - Your balance is low. - </h2> - <p className="px-4 py-2 text-center text-sm font-medium text-white"> - You may continue to attempt surveys, but you will be - advised to return the HIT if your survey attempt does - not result in a complete. - </p> - <p className="px-4 py-2 text-center text-sm font-bold text-white"> - HIT submissions with a balance exceeding -$1.00 will - return in rejected assignments. - </p> - </div> - )} - - <LOISelect /> - <div - onClick={start_survey} - className=" + bg-rose-400 text-rose-700" + > + <h2 className="text-center text-xl font-bold text-white"> + Your balance is low. + </h2> + <p className="px-4 py-2 text-center text-sm font-medium text-white"> + You may continue to attempt surveys, but you will not earn an + attempt credit if your survey attempt does not result in a complete. + </p> + <p className="px-4 py-2 text-center text-sm font-bold text-white"> + Attempts with a balance exceeding -$1.00 will not be issued an + attempt credit. + </p> + </div> + )} + + <LOISelect /> + <div + onClick={start_survey} + className=" border-8 border-emerald-400 rounded py-0 my-2 - hover:cursor-pointer hover:bg-emerald-400 hover:text-emerald-50"> - <p className="py-2 text-center text-3xl font-semibold"> - <a - target="_blank" - rel="noopener noreferrer"> - Start Task - </a> - </p> - </div> - <BucketDetails /> - - <div className="border-2 border-emerald-200 rounded py-2 m-0"> - <p className="text-emerald-700 text-sm text-center font-medium px-2 m-0"> - <Markdown> - {bucket.eligibility_explanation} - </Markdown> - </p> - </div> - </> - ) + hover:cursor-pointer hover:bg-emerald-400 hover:text-emerald-50" + > + <p className="py-2 text-center text-3xl font-semibold"> + <a target="_blank" rel="noopener noreferrer"> + Start Task + </a> + </p> + </div> + <BucketDetails /> + + <div className="border-2 border-emerald-200 rounded py-2 m-0"> + <p className="text-emerald-700 text-sm text-center font-medium px-2 m-0"> + <Markdown>{bucket.eligibility_explanation}</Markdown> + </p> + </div> + </> + ); }; - const Work = () => { - const entered = useAppSelector(state => state.app.currentBucketEntered) - const assignment_id = useAppSelector(state => state.app.assignment_id) - - if (!assignment_id) { - return ( - <p className="text-center text-2xl py-3 text-red-800 font-semibold"> - No Assignment ID found. Please access this page through - the Amazon Mechanical Turk platform. - </p> - ) - } - - return ( - <> - {entered ? ( - <SurveyMonitor /> - ) : ( - <SurveyEnter /> - )} - </> - ) + const entered = useAppSelector((state) => state.app.currentBucketEntered); + // const assignment_id = useAppSelector((state) => state.app.assignment_id); + // if (!assignment_id) { + // return ( + // <p className="text-center text-2xl py-3 text-red-800 font-semibold"> + // No Assignment ID found. Please access this page through the Amazon + // Mechanical Turk platform. + // </p> + // ); + // } + + const { data: user, isLoading, isError } = useGetMeQuery(); + // const location = useLocation(); + + if (isLoading) return <div>Loading...</div>; + + if (isError || !user) { + return <Preview />; + } + + return <>{entered ? <SurveyMonitor /> : <SurveyEnter />}</>; }; - -export default Work;
\ No newline at end of file +export default Work; diff --git a/jb-ui/src/store.ts b/jb-ui/src/store.ts index 7b4d3e3..081de9f 100644 --- a/jb-ui/src/store.ts +++ b/jb-ui/src/store.ts @@ -1,32 +1,38 @@ -import appReducers from "@/models/appSlice" -import grlStatsReducers from "@/models/grlStatsSlice" -import grlEventsReducers from "@/models/grlEventsSlice" -import profilingQuestionsReducers from "@/models/profilingQuestionsSlice" -import type { Action, ThunkAction } from '@reduxjs/toolkit' -import { configureStore } from '@reduxjs/toolkit' +import appReducers from "@/models/appSlice"; +import grlStatsReducers from "@/models/grlStatsSlice"; +import grlEventsReducers from "@/models/grlEventsSlice"; +import { authApi } from "@/models/authAPI"; +import profilingQuestionsReducers from "@/models/profilingQuestionsSlice"; +import cashoutMethodReducers from "@/models/cashoutMethodsSlice"; +import type { Action, ThunkAction } from "@reduxjs/toolkit"; +import { configureStore } from "@reduxjs/toolkit"; export const store = configureStore({ - reducer: { - app: appReducers, - stats: grlStatsReducers, - events: grlEventsReducers, - profilingQuestions: profilingQuestionsReducers, - } -}) + reducer: { + app: appReducers, + // auth: authReducers, + [authApi.reducerPath]: authApi.reducer, + stats: grlStatsReducers, + events: grlEventsReducers, + profilingQuestions: profilingQuestionsReducers, + cashoutMethods: cashoutMethodReducers, + }, + middleware: (getDefault) => getDefault().concat(authApi.middleware), +}); // Infer the type of `store` -export type AppStore = typeof store +export type AppStore = typeof store; // Infer the `AppDispatch` type from the store itself -export type AppDispatch = typeof store.dispatch +export type AppDispatch = typeof store.dispatch; // Same for the `RootState` type -export type RootState = ReturnType<typeof store.getState> +export type RootState = ReturnType<typeof store.getState>; // Define a reusable type describing thunk functions export type AppThunk<ThunkReturnType = void> = ThunkAction< - ThunkReturnType, - RootState, - unknown, - Action -> + ThunkReturnType, + RootState, + unknown, + Action +>; diff --git a/jb-ui/src/types/authApi.ts b/jb-ui/src/types/authApi.ts new file mode 100644 index 0000000..19aeee0 --- /dev/null +++ b/jb-ui/src/types/authApi.ts @@ -0,0 +1,6 @@ +export interface User { + email: string; + display_name: string | null; + blocked: boolean; + product_user_id: string; +} diff --git a/jb-ui/src/vite-env.d.ts b/jb-ui/src/vite-env.d.ts index 11d7a7a..93dc3cc 100644 --- a/jb-ui/src/vite-env.d.ts +++ b/jb-ui/src/vite-env.d.ts @@ -1,17 +1,17 @@ /// <reference types="vite/client" /> interface ViteTypeOptions { - // By adding this line, you can make the type of ImportMetaEnv strict - // to disallow unknown keys. - // strictImportMetaEnv: unknown + // By adding this line, you can make the type of ImportMetaEnv strict + // to disallow unknown keys. + // strictImportMetaEnv: unknown } interface ImportMetaEnv { - readonly VITE_BASENAME: string - readonly VITE_BPID: string - readonly VITE_TAG_MANAGER: string + readonly VITE_BASENAME: string; + readonly VITE_BPID: string; + readonly VITE_TAG_MANAGER: string; } interface ImportMeta { - readonly env: ImportMetaEnv -}
\ No newline at end of file + readonly env: ImportMetaEnv; +} diff --git a/jb-ui/tests/base.test.ts b/jb-ui/tests/base.test.ts index e2f56a6..cd818d9 100644 --- a/jb-ui/tests/base.test.ts +++ b/jb-ui/tests/base.test.ts @@ -1,3 +1,3 @@ -test('1 + 1 equals 2', () => { - expect(1 + 1).toBe(2); -});
\ No newline at end of file +test("1 + 1 equals 2", () => { + expect(1 + 1).toBe(2); +}); diff --git a/jb-ui/tests/setupTests.ts b/jb-ui/tests/setupTests.ts index 8f2609b..1dd407a 100644 --- a/jb-ui/tests/setupTests.ts +++ b/jb-ui/tests/setupTests.ts @@ -2,4 +2,4 @@ // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; +import "@testing-library/jest-dom"; diff --git a/jb-ui/vite.config.ts b/jb-ui/vite.config.ts index 6b0e530..2336e4a 100644 --- a/jb-ui/vite.config.ts +++ b/jb-ui/vite.config.ts @@ -1,62 +1,56 @@ -import react from "@vitejs/plugin-react" -import {defineConfig} from "vite" -import path from 'path'; -import tailwindcss from '@tailwindcss/vite' - +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; +import path from "path"; +import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ - plugins: [ - react(), - tailwindcss(), - - ], + plugins: [react(), tailwindcss()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), }, - - server: { - // host: "0.0.0.0", - // port: 8001, - - // This forwards requests to the FastAPI development server - // that must also be running - proxy: { - '^/(status|statuses|report|survey)(/|$)': { - target: 'http://localhost:8000', - changeOrigin: true, - } - } + }, + + server: { + // host: "0.0.0.0", + // port: 8001, + + // This forwards requests to the FastAPI development server + // that must also be running + proxy: { + "^/(auth|status|statuses|report|survey)(/|$)": { + target: "http://localhost:8000", + changeOrigin: true, + }, }, + }, - define: { - 'process.env.NODE_ENV': JSON.stringify('production') - }, + define: { + "process.env.NODE_ENV": JSON.stringify("production"), + }, - build: { - cssCodeSplit: false, - lib: { - name: "jb-ui", - entry: 'src/main.tsx', + build: { + cssCodeSplit: false, + lib: { + name: "jb-ui", + entry: "src/main.tsx", + }, + rollupOptions: { + output: { + entryFileNames: "james-billings.js", + assetFileNames: (assetInfo) => { + if (assetInfo.names[0].endsWith(".css")) { + return "james-has-style.css"; + } + + return "assets/[name].[hash][extname]"; }, - rollupOptions: { - output: { - entryFileNames: 'james-billings.js', - assetFileNames: (assetInfo) => { - - if (assetInfo.names[0].endsWith('.css')) { - return 'james-has-style.css'; - } - - return 'assets/[name].[hash][extname]'; - }, - globals: { - react: 'React', - 'react-dom': 'ReactDOM' - } - } - } - } -})
\ No newline at end of file + globals: { + react: "React", + "react-dom": "ReactDOM", + }, + }, + }, + }, +}); diff --git a/carer/carer/mtwerk/migrations/__init__.py b/jb/api/__init__.py index e69de29..e69de29 100644 --- a/carer/carer/mtwerk/migrations/__init__.py +++ b/jb/api/__init__.py diff --git a/jb/api/auth.py b/jb/api/auth.py new file mode 100644 index 0000000..1542e70 --- /dev/null +++ b/jb/api/auth.py @@ -0,0 +1,88 @@ +from datetime import datetime, timedelta, timezone +from typing import Annotated +from uuid import uuid4 + +import jwt +from fastapi import Depends, HTTPException, Request, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + +from jb.config import settings +from jb.dependencies import get_gr_api_manager +from jb.managers.gr_api import GRApiManager +from jb.models.auth import User + +bearer = HTTPBearer(auto_error=False) + +SESSION_COOKIE_NAME = "jb_session" +JWT_ISSUER = "jamesbillings67" +JWT_AUDIENCE = "jamesbillings67" + + +def get_authenticated_user( + request: Request, + credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)], + gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], +) -> User: + """FastAPI dependency for endpoints requiring a valid session.""" + if settings.session_jwt_secret is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Account session signing key is not configured", + ) + + token = request.cookies.get(SESSION_COOKIE_NAME) + if credentials is not None and credentials.scheme.lower() == "bearer": + token = credentials.credentials + + if token is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Missing session token", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + claims = jwt.decode( + token, + settings.session_jwt_secret.get_secret_value(), + algorithms=["HS256"], + issuer=JWT_ISSUER, + audience=JWT_AUDIENCE, + options={"require": ["sub", "iat", "exp", "jti", "iss", "aud", "type"]}, + ) + except jwt.PyJWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or expired session token", + headers={"WWW-Authenticate": "Bearer"}, + ) + + if claims["type"] != "session" or not isinstance(claims["sub"], str): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or expired session token", + headers={"WWW-Authenticate": "Bearer"}, + ) + product_user_id = claims.get("sub") + + user = gr_api.get_user(product_user_id=product_user_id) + + return user + + +def create_session(product_user_id: str) -> str: + now = datetime.now(timezone.utc) + assert settings.session_jwt_secret + return jwt.encode( + { + "sub": product_user_id, + "iat": now, + "exp": now + timedelta(seconds=settings.session_token_ttl_seconds), + "jti": uuid4().hex, + "iss": JWT_ISSUER, + "aud": JWT_AUDIENCE, + "type": "session", + }, + settings.session_jwt_secret.get_secret_value(), + algorithm="HS256", + ) diff --git a/jb/api/magic_token.py b/jb/api/magic_token.py new file mode 100644 index 0000000..b60d5e9 --- /dev/null +++ b/jb/api/magic_token.py @@ -0,0 +1,96 @@ +import hashlib +import secrets + +from fastapi import HTTPException, status +from generalresearch.redis_helper import RedisConfig + +from jb.decorators import get_redis_config +from jb.models.auth import AmtAccountLink + +MAGIC_TOKEN_PREFIX = "auth:magic:" +AMT_ACCOUNT_LINK_TOKEN_PREFIX = "auth:amt-account-link:" +MAGIC_TOKEN_TTL: int = 10 * 60 # 10 minutes, in seconds + + +def redis_token_key(token: str, prefix: str = MAGIC_TOKEN_PREFIX) -> str: + # Redis never contains a usable credential, even if its keys are exposed. + digest = hashlib.sha256(token.encode("utf-8")).hexdigest() + return f"{prefix}{digest}" + + +def create_magic_token(user_email: str, redis_config: RedisConfig | None = None) -> str: + """Create a short-lived, single-use token for a user. + The raw token can then be sent by email. + """ + if not user_email or not user_email.strip(): + raise ValueError("user_email must not be empty") + + token = secrets.token_urlsafe(32) + + if redis_config is None: + redis_config = get_redis_config() + + redis_client = redis_config.create_redis_client() + + redis_client.set( + redis_token_key(token), + user_email, + ex=MAGIC_TOKEN_TTL, + ) + return token + + +def consume_magic_token(token: str, redis_config: RedisConfig | None = None) -> str: + if redis_config is None: + redis_config = get_redis_config() + + redis_client = redis_config.create_redis_client() + + user_email = redis_client.getdel(redis_token_key(token)) + if user_email is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or expired magic token", + ) + return str(user_email) + + +def create_amt_account_link_token( + email: str, amt_worker_id: str, redis_config: RedisConfig | None = None +) -> str: + """Bind an email and AMT worker ID to an opaque, short-lived token.""" + if redis_config is None: + redis_config = get_redis_config() + + redis_client = redis_config.create_redis_client() + + data = AmtAccountLink( + email=email, + amt_worker_id=amt_worker_id, + ) + token = secrets.token_urlsafe(32) + redis_client.set( + redis_token_key(token, AMT_ACCOUNT_LINK_TOKEN_PREFIX), + data.model_dump_json(), + ex=MAGIC_TOKEN_TTL, + ) + return token + + +def consume_amt_account_link_token( + token: str, redis_config: RedisConfig | None = None +) -> AmtAccountLink: + """Atomically consume and validate an AMT account-link token.""" + if redis_config is None: + redis_config = get_redis_config() + redis_client = redis_config.create_redis_client() + + raw_data = redis_client.getdel( + redis_token_key(token, AMT_ACCOUNT_LINK_TOKEN_PREFIX) + ) + if raw_data is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or expired account-link token", + ) + return AmtAccountLink.model_validate_json(raw_data) diff --git a/jb/config.py b/jb/config.py index c7d07e5..7993f53 100644 --- a/jb/config.py +++ b/jb/config.py @@ -1,17 +1,8 @@ import logging -from generalresearchutils.config import is_debug +from jb.settings import get_settings -from jb.settings import get_settings, get_test_settings - -if is_debug(): - print("running using TEST settings") - settings = get_test_settings() - assert settings.debug is True -else: - print("running using PROD settings") - settings = get_settings() - assert settings.debug is False +settings = get_settings() if settings.debug: LOG_LEVEL = logging.DEBUG @@ -30,7 +21,6 @@ SUBSCRIPTION = { } JB_EVENTS_STREAM = "amt_jb_events" -JB_EVENTS_FAILED_STREAM = "amt_jb_events_failed" CONSUMER_GROUP = "amt-jb-0" # We'll only have 1 consumer atm, change this if we don't CONSUMER_NAME = "amt-jb-0" diff --git a/jb/decorators.py b/jb/decorators.py index 5c1b1f5..9c7a31c 100644 --- a/jb/decorators.py +++ b/jb/decorators.py @@ -1,17 +1,20 @@ +import logging + import boto3 from botocore.config import Config -from generalresearchutils.pg_helper import PostgresConfig -from generalresearchutils.redis_helper import RedisConfig +from generalresearch.managers.base import Permission +from generalresearch.pg_helper import PostgresConfig +from generalresearch.redis_helper import RedisConfig from influxdb import InfluxDBClient from mypy_boto3_mturk import MTurkClient from mypy_boto3_sns import SNSClient from jb.config import settings -from jb.managers import Permission from jb.managers.amt import AMTManager from jb.managers.assignment import AssignmentManager from jb.managers.bonus import BonusManager -from jb.managers.hit import HitTypeManager, HitManager, HitQuestionManager +from jb.managers.gr_api import GRApiManager +from jb.managers.hit import HitManager, HitQuestionManager, HitTypeManager redis_config = RedisConfig( dsn=settings.redis, @@ -19,7 +22,24 @@ redis_config = RedisConfig( socket_timeout=settings.redis_timeout, socket_connect_timeout=settings.redis_timeout, ) -REDIS = redis_config.create_redis_client() + + +def get_redis_config(): + return redis_config + + +def get_redis(): + return redis_config.create_redis_client() + + +# --- Logging --- + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s:%(name)s:%(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) +LOG = logging.getLogger("amtjb") CLIENT_CONFIG = Config( # connect_timeout (float or int) – The time in seconds till a timeout @@ -32,6 +52,12 @@ CLIENT_CONFIG = Config( read_timeout=2.5, ) +gr_api_manager = GRApiManager( + base_url=str(settings.gr_api_host), + token=settings.gr_api_token.get_secret_value(), + product_id=settings.product_id, +) + # We shouldn't use this directly. Use our AMTManager wrapper AMT_CLIENT: MTurkClient = boto3.client( service_name="mturk", diff --git a/jb/dependencies.py b/jb/dependencies.py new file mode 100644 index 0000000..b9577bc --- /dev/null +++ b/jb/dependencies.py @@ -0,0 +1,8 @@ +"""FastAPI dependency providers shared by application routes.""" + +from jb.decorators import gr_api_manager +from jb.managers.gr_api import GRApiManager + + +def get_gr_api_manager() -> GRApiManager: + return gr_api_manager diff --git a/jb/flow/assignment_tasks.py b/jb/flow/assignment_tasks.py index 18a844e..b3c820a 100644 --- a/jb/flow/assignment_tasks.py +++ b/jb/flow/assignment_tasks.py @@ -1,37 +1,15 @@ -import logging -import math -from datetime import timedelta -from typing import Optional - -from generalresearchutils.models.thl.definitions import PayoutStatus, StatusCode1 -from generalresearchutils.models.thl.wallet.cashout_method import CashoutRequestInfo -from generalresearchutils.currency import USDCent - -from jb.flow.monitoring import emit_error_event, emit_assignment_event, emit_bonus_event +from jb.decorators import LOG +from jb.flow.monitoring import emit_assignment_event, emit_error_event from jb.managers.amt import ( - AMTManager, REJECT_MESSAGE_UNKNOWN_ASSIGNMENT, - REJECT_MESSAGE_NO_WORK, - NO_WORK_APPROVAL_MESSAGE, - REJECT_MESSAGE_BADDIE, - APPROVAL_MESSAGE, - BONUS_MESSAGE, -) -from jb.managers.thl import ( - get_user_blocked, - get_task_status, - user_cashout_request, - manage_pending_cashout, - get_user_blocked_or_not_exists, - get_wallet_balance_if_non_negative, + AMTManager, ) +from jb.managers.assignment import AssignmentManager +from jb.managers.bonus import BonusManager +from jb.managers.hit import HitManager from jb.models.assignment import Assignment from jb.models.definitions import AssignmentStatus from jb.models.event import MTurkEvent -from jb.managers.assignment import AssignmentManager -from jb.managers.hit import HitManager -from jb.managers.bonus import BonusManager -from jb.config import settings def process_assignment_submitted( @@ -42,16 +20,13 @@ def process_assignment_submitted( event: MTurkEvent, ) -> None: """ - Called either directly or from the SNS Notification that a - HIT was submitted - - :return: None + Reject any submitted assignments """ # # Step 1: Attempt to get the Assignment out of the API # - logging.info(f"{event=}") + LOG.info(f"{event=}") # This is the assignment model from AMT. In the DB, we should only have # the AssignmentStub @@ -66,95 +41,26 @@ def process_assignment_submitted( # It is not found in amt, either it is invalid, not yet submitted, or # already been approved/rejected, so we just do nothing ... # todo: maybe we confirm its state matches what we have in the db - logging.warning(f"No assignment found on AMT: {event.amt_assignment_id}") + LOG.warning(f"No assignment found on AMT: {event.amt_assignment_id}") emit_error_event( event_type="assignment_not_found_in_amt", amt_hit_type_id=event.amt_hit_type_id, ) - return None + return # Even if the assignment doesn't exist, the hit must ... hit = hm.get_from_amt_id(amt_hit_id=assignment.amt_hit_id) - # - # Step 2: Attempt to get the Assignment out of the DB - # - # Now, we need to confirm it is something that we have in the db. If not, - # that means either something broke, or some funny business is happening - # (maybe a baddie is submitting an assignment without doing any work). - stub = am.get_stub_if_exists(amt_assignment_id=assignment.amt_assignment_id) - if stub is None: - # When they visited the "work" page, it should have created an - # AssignmentStub in the db. If that doesn't exist, something bad - # happened. - logging.warning(f"No assignment found in DB: {event.amt_assignment_id}") - emit_error_event( - event_type="assignment_stub_not_found_in_db", - amt_hit_type_id=event.amt_hit_type_id, - ) - reject_assignment( - amtm=amtm, - am=am, - hm=hm, - amt_assignment_id=assignment.amt_assignment_id, - msg=REJECT_MESSAGE_UNKNOWN_ASSIGNMENT, - amt_hit_type_id=hit.amt_hit_type_id, - ) - review_hit(amtm=amtm, hm=hm, assignment=assignment) - return None - - assert assignment.amt_assignment_id == event.amt_assignment_id - assert assignment.amt_hit_id == event.amt_hit_id - assert assignment.amt_hit_id == stub.amt_hit_id - assert assignment.amt_worker_id == stub.amt_worker_id - amt_assignment_id = assignment.amt_assignment_id - amt_worker_id = assignment.amt_worker_id - - # We don't have a TSID associated with the assignment until we the - # assignment is submitted. - am.update_answer(assignment=assignment) - - # check if the user is blocked by thl - if get_user_blocked_or_not_exists(amt_worker_id=amt_worker_id): - logging.warning( - f"User {amt_worker_id} blocked or not exists. Rejecting: {amt_assignment_id}" - ) - emit_error_event( - event_type="assignment_submitted_user_blocked_or_not_exists", - amt_hit_type_id=event.amt_hit_type_id, - ) - reject_assignment( - amtm=amtm, - am=am, - hm=hm, - amt_assignment_id=amt_assignment_id, - msg=REJECT_MESSAGE_BADDIE, - amt_hit_type_id=hit.amt_hit_type_id, - ) - review_hit(amtm=amtm, hm=hm, assignment=assignment) - return None - - if assignment.tsid is None: - assignment = handle_assignment_w_no_work( - amtm=amtm, am=am, hm=hm, assignment=assignment - ) - else: - # We need to validate the work exists on thl, and if so, approve - assignment = handle_assignment_w_work( - amtm=amtm, am=am, hm=hm, assignment=assignment - ) - - # - # Step 4: Tell Amazon we've reviewed the HIT, and update the DB - # + reject_assignment( + amtm=amtm, + am=am, + hm=hm, + amt_assignment_id=assignment.amt_assignment_id, + msg=REJECT_MESSAGE_UNKNOWN_ASSIGNMENT, + amt_hit_type_id=hit.amt_hit_type_id, + ) review_hit(amtm=amtm, hm=hm, assignment=assignment) - - if ( - assignment.tsid - and assignment.status == AssignmentStatus.Approved - and assignment.requester_feedback != NO_WORK_APPROVAL_MESSAGE - ): - return issue_worker_payment(amtm=amtm, hm=hm, bm=bm, assignment=assignment) + return def review_hit(amtm: AMTManager, hm: HitManager, assignment: Assignment) -> None: @@ -163,66 +69,13 @@ def review_hit(amtm: AMTManager, hm: HitManager, assignment: Assignment) -> None hit, _ = amtm.get_hit_if_exists(amt_hit_id=assignment.amt_hit_id) if hit is None: - logging.warning( - f"Hit not found when trying to review hit: {assignment.amt_hit_id}" - ) - return None + LOG.warning(f"Hit not found when trying to review hit: {assignment.amt_hit_id}") + return # Update the db hm.update_hit(hit) - return None - - -def handle_assignment_w_no_work( - amtm: AMTManager, am: AssignmentManager, hm: HitManager, assignment: Assignment -) -> Assignment: - """ - Called when an assignment is submitted without a wall event. - Not entirely clear why this happens. I think they accept a HIT, get no work - available for whatever reason, then report, and submit it. - - :return: The Assignment - """ - logging.warning( - f"Assignment submitted with no tsid: {assignment.amt_assignment_id}" - ) - amt_worker_id = assignment.amt_worker_id - amt_assignment_id = assignment.amt_assignment_id - hit = hm.get_from_amt_id(amt_hit_id=assignment.amt_hit_id) - emit_error_event( - event_type="assignment_submitted_no_work", - amt_hit_type_id=hit.amt_hit_type_id, - ) - - # They get 0 chances due to abuse - if True: - # if (am.missing_tsid_count(amt_worker_id=amt_worker_id) >= 3) or ( - # am.rejected_count(amt_worker_id=amt_worker_id) >= 3 - # or get_user_blocked(amt_worker_id=amt_worker_id) - # ): - assignment = reject_assignment( - amtm=amtm, - am=am, - hm=hm, - amt_assignment_id=amt_assignment_id, - msg=REJECT_MESSAGE_NO_WORK, - amt_hit_type_id=hit.amt_hit_type_id, - ) - # todo: we don't have a way to "block" a user (i.e. tattle to thl) - # make_block_worker_decision(user) - return assignment - - # Approve with a message explaining they shouldn't do it. - assignment = approve_assignment( - amtm=amtm, - am=am, - amt_assignment_id=amt_assignment_id, - msg=NO_WORK_APPROVAL_MESSAGE, - amt_hit_type_id=hit.amt_hit_type_id, - ) - - return assignment + return def reject_assignment( @@ -245,7 +98,7 @@ def reject_assignment( event_type="failed_to_reject_assignment", amt_hit_type_id=amt_hit_type_id, ) - logging.exception(f"Failed to reject assignment: {amt_assignment_id}") + LOG.exception(f"Failed to reject assignment: {amt_assignment_id}") # We just rejected this assignment, get it from amazon again assignment = amtm.get_assignment(amt_assignment_id=amt_assignment_id) @@ -256,7 +109,7 @@ def reject_assignment( # need to create as assignment first ... stub = am.get_stub_if_exists(amt_assignment_id=assignment.amt_assignment_id) if stub is None: - logging.warning( + LOG.warning( f"Rejected assignment doesn't exist in DB. Creating ... : {amt_assignment_id}" ) # Even if the assignment doesn't exist, the hit must ... @@ -267,276 +120,5 @@ def reject_assignment( emit_assignment_event( status=AssignmentStatus.Rejected, amt_hit_type_id=amt_hit_type_id, reason=msg ) - logging.warning(f"Rejected assignment: {amt_assignment_id}") + LOG.warning(f"Rejected assignment: {amt_assignment_id}") return assignment - - -def approve_assignment( - amtm: AMTManager, - am: AssignmentManager, - amt_assignment_id: str, - msg: str, - amt_hit_type_id: str, - override_rejection: bool = False, -) -> Assignment: - # Approve in AMT, update db - - res = amtm.approve_assignment_if_possible( - amt_assignment_id=amt_assignment_id, - msg=msg, - override_rejection=override_rejection, - ) - if res is None: - # We failed to approve this assignment. Cannot distinguish between - # failed b/c assignment is already approved, or it is not possible. - emit_error_event( - event_type="failed_to_approve_assignment", - amt_hit_type_id=amt_hit_type_id, - ) - # The assignment might already be approved, the error msg is useless, so - # keep going. - # raise Exception(f"Failed to approve assignment: {amt_assignment_id}") - - # We just approved this assignment, get it from amazon again - assignment = amtm.get_assignment(amt_assignment_id=amt_assignment_id) - assert assignment.status == AssignmentStatus.Approved - # And update the db - am.approve(assignment=assignment) - emit_assignment_event( - status=AssignmentStatus.Approved, amt_hit_type_id=amt_hit_type_id, reason=msg - ) - logging.warning(f"Approved assignment: {amt_assignment_id}") - return assignment - - -def handle_assignment_w_work( - amtm: AMTManager, am: AssignmentManager, hm: HitManager, assignment: Assignment -) -> Assignment: - """ - Called when an assignment is submitted with a tsid. - - Check the tsid (thl status endpoint). Make sure it is finished, and - stuff matches (doesn't matter if not a complete) - - Try to submit a cashout request for the HIT payout (e.g. 5c) - """ - - amt_worker_id = assignment.amt_worker_id - amt_assignment_id = assignment.amt_assignment_id - tsid = assignment.tsid - assert ( - tsid is not None - ), "Assignment must have a tsid to be handled in handle_assignment_w_work" - - hit = hm.get_from_amt_id(amt_hit_id=assignment.amt_hit_id) - - tsr = get_task_status(tsid=tsid) - if ( - tsr is None - or tsr.status is None - or tsr.status_code_1 - in { - StatusCode1.SESSION_START_FAIL, - StatusCode1.SESSION_START_QUALITY_FAIL, - StatusCode1.SESSION_CONTINUE_QUALITY_FAIL, - } - ): - # TSID doesn't exist or work is not finished: - # Reject the assignment instead - if tsr is not None and tsr.status_code_1 in { - StatusCode1.SESSION_START_QUALITY_FAIL, - StatusCode1.SESSION_CONTINUE_QUALITY_FAIL, - }: - event_type = "assignment_submitted_quality_fail" - else: - event_type = "assignment_submitted_work_not_complete" - - emit_error_event( - event_type=event_type, - amt_hit_type_id=hit.amt_hit_type_id, - ) - assignment = reject_assignment( - amtm=amtm, - am=am, - hm=hm, - amt_assignment_id=amt_assignment_id, - msg=REJECT_MESSAGE_BADDIE, - amt_hit_type_id=hit.amt_hit_type_id, - ) - return assignment - - assert tsr.product_user_id == amt_worker_id - assert tsr.finished is not None - assert (tsr.finished - assignment.created_at) <= timedelta(minutes=90) - - # Request an AMT_ASSIGNMENT cashout for 1c - req = submit_and_approve_amt_assignment_request( - amt_worker_id=amt_worker_id, amount=hit.reward - ) - if req is None: - # Reject the assignment instead - logging.warning( - f"submit_and_approve_amt_assignment_request failed: {amt_assignment_id}" - ) - emit_error_event( - event_type="assignment_cashout_request_failed", - amt_hit_type_id=hit.amt_hit_type_id, - ) - assignment = reject_assignment( - amtm=amtm, - am=am, - hm=hm, - amt_assignment_id=amt_assignment_id, - msg=REJECT_MESSAGE_BADDIE, - amt_hit_type_id=hit.amt_hit_type_id, - ) - return assignment - - assert req.id - - # We've approved the HIT payment, now update the db to reflect this, and approve the assignment - assignment = approve_assignment( - amtm=amtm, - am=am, - amt_assignment_id=amt_assignment_id, - msg=APPROVAL_MESSAGE, - amt_hit_type_id=hit.amt_hit_type_id, - ) - # We complete after the assignment is approved - complete_res = manage_pending_cashout( - cashout_id=req.id, payout_status=PayoutStatus.COMPLETE - ) - if complete_res.status != PayoutStatus.COMPLETE: - # unclear wny this would happen - raise ValueError(f"Failed to complete cashout: {req.id}") - return assignment - - -def submit_and_approve_amt_assignment_request( - amt_worker_id: str, amount: USDCent -) -> Optional[CashoutRequestInfo]: - # If successful, returns the cashout id, otherwise, returns None - req = user_cashout_request( - amt_worker_id=amt_worker_id, - amount=amount, - cashout_method_id=settings.amt_assignment_cashout_method, - ) - assert req.id - - if req.status != PayoutStatus.PENDING: - return None - - approve_res = manage_pending_cashout(req.id, PayoutStatus.APPROVED) - if approve_res.status != PayoutStatus.APPROVED: - return None - - return req - - -def submit_and_approve_amt_bonus_request( - amt_worker_id: str, amount: USDCent -) -> Optional[CashoutRequestInfo]: - # If successful, returns the cashout id, otherwise, returns None - req = user_cashout_request( - amt_worker_id=amt_worker_id, - amount=amount, - cashout_method_id=settings.amt_bonus_cashout_method, - ) - assert req.id - - if req.status != PayoutStatus.PENDING: - return None - - approve_res = manage_pending_cashout(req.id, PayoutStatus.APPROVED) - if approve_res.status != PayoutStatus.APPROVED: - return None - - return req - - -def issue_worker_payment( - amtm: AMTManager, hm: HitManager, bm: BonusManager, assignment: Assignment -) -> None: - # For now, since we have no "I want my bonus" request/button. A user's - # balance will be sent out anytime they get an approved assignment. We - # don't need the task status, the tsid, nor the amount / user_payout - # that was paid, or anything. - # We just get the wallet balance and submit a cashout request if >0 - # then approve it, send the amt bonus, then complete it - amt_assignment_id = assignment.amt_assignment_id - hit = hm.get_from_amt_id(amt_hit_id=assignment.amt_hit_id) - wallet_balance = get_wallet_balance_if_non_negative( - amt_worker_id=assignment.amt_worker_id - ) - if not wallet_balance: - return None - amount = round_payment(amount=wallet_balance) - if not amount: - return None - - # Don't send more than $4.97 at a time. If they have a higher wallet balance, - # they just need to get another hit approved. - amount = min(amount, USDCent(4_97)) - - pe = submit_and_approve_amt_bonus_request( - amt_worker_id=assignment.amt_worker_id, amount=amount - ) - if pe is None: - logging.warning( - f"submit_and_approve_amt_bonus_request failed: {amt_assignment_id}" - ) - emit_error_event( - event_type="bonus_cashout_request_failed", - amt_hit_type_id=hit.amt_hit_type_id, - ) - return None - assert pe.id - - amtm.send_bonus( - amt_worker_id=assignment.amt_worker_id, - amt_assignment_id=assignment.amt_assignment_id, - amount=amount, - reason=BONUS_MESSAGE, - unique_request_token=pe.id, - ) - - # Confirm it was sent through amt - bonus = amtm.get_bonus( - amt_assignment_id=assignment.amt_assignment_id, payout_event_id=pe.id - ) - - if bonus is None: - logging.warning( - f"Failed to find bonus after sending it: {amt_assignment_id} {pe.id}" - ) - emit_error_event( - event_type="bonus_not_found_after_sending", - amt_hit_type_id=hit.amt_hit_type_id, - ) - return None - - # Create in DB - bm.create(bonus=bonus) - emit_bonus_event(amount=amount, amt_hit_type_id=hit.amt_hit_type_id) - - # Complete cashout - res = manage_pending_cashout(pe.id, PayoutStatus.COMPLETE) - if res.status != PayoutStatus.COMPLETE: - raise ValueError( - f"{assignment.amt_assignment_id} {pe.id=} manage_pending_cashout COMPLETE failed: {res=}" - ) - - -def round_payment(amount: USDCent) -> USDCent: - """ - Don't pay bonuses less than 7 cents, just add it to their wallet. - Round down bonuses (>=7 cents) to the nearest multiple of 5 - starting at 2 - """ - if amount < 7: - return USDCent(0) - - amt = (5 * math.floor((int(amount) - 2) / 5)) + 2 - - payout = USDCent(amt) - assert 0 <= payout <= 40_00, "Payout must be between $0.00 and $40.00" - - return payout diff --git a/jb/flow/events.py b/jb/flow/events.py index 2825cb1..bee63fd 100644 --- a/jb/flow/events.py +++ b/jb/flow/events.py @@ -1,18 +1,16 @@ -import logging import time from concurrent import futures -from concurrent.futures import ThreadPoolExecutor, Executor -from typing import Optional, cast, TypedDict +from concurrent.futures import Executor, ThreadPoolExecutor +from typing import cast import redis from jb.config import ( - JB_EVENTS_STREAM, CONSUMER_GROUP, CONSUMER_NAME, - JB_EVENTS_FAILED_STREAM, + JB_EVENTS_STREAM, ) -from jb.decorators import REDIS +from jb.decorators import LOG, get_redis from jb.flow.assignment_tasks import process_assignment_submitted from jb.flow.monitoring import emit_error_event from jb.models.event import MTurkEvent @@ -20,13 +18,6 @@ from jb.models.event import MTurkEvent StreamMessages = list[tuple[str, list[tuple[bytes, dict[bytes, bytes]]]]] -class PendingEntry(TypedDict): - message_id: bytes - consumer: bytes - time_since_delivered: int - times_delivered: int - - def process_mturk_events_task(): executor = ThreadPoolExecutor(max_workers=5) create_consumer_group() @@ -34,21 +25,11 @@ def process_mturk_events_task(): try: process_mturk_events(executor=executor) except Exception as e: - logging.exception(e) + LOG.exception(e) finally: time.sleep(1) -def handle_pending_msgs_task(): - while True: - try: - handle_pending_msgs() - except Exception as e: - logging.exception(e) - finally: - time.sleep(60) - - def process_mturk_events(executor: Executor): while True: n = process_mturk_events_chunk(executor=executor) @@ -58,7 +39,10 @@ def process_mturk_events(executor: Executor): def create_consumer_group(): try: - REDIS.xgroup_create(JB_EVENTS_STREAM, CONSUMER_GROUP, id="0", mkstream=True) + redis_client = get_redis() + redis_client.xgroup_create( + JB_EVENTS_STREAM, CONSUMER_GROUP, id="0", mkstream=True + ) except redis.exceptions.ResponseError as e: if "BUSYGROUP Consumer Group name already exists" in str(e): pass # group already exists @@ -66,8 +50,9 @@ def create_consumer_group(): raise -def process_mturk_events_chunk(executor: Executor) -> Optional[int]: - msgs_raw = REDIS.xreadgroup( +def process_mturk_events_chunk(executor: Executor) -> int | None: + redis_client = get_redis() + msgs_raw = redis_client.xreadgroup( groupname=CONSUMER_GROUP, consumername=CONSUMER_NAME, streams={JB_EVENTS_STREAM: ">"}, @@ -89,66 +74,25 @@ def process_mturk_events_chunk(executor: Executor) -> Optional[int]: executor.submit(process_assignment_submitted_event, event, str(msg_id)) ) else: - logging.info(f"Discarding {event}") - REDIS.xdel(JB_EVENTS_STREAM, msg_id) + LOG.info(f"Discarding {event}") + redis_client.xdel(JB_EVENTS_STREAM, msg_id) futures.wait(fs, timeout=60) return len(msgs) def process_assignment_submitted_event(event: MTurkEvent, msg_id: str): - from jb.decorators import AMTM, AM, HM, BM + from jb.decorators import AM, AMTM, BM, HM + + redis_client = get_redis() try: process_assignment_submitted(amtm=AMTM, am=AM, hm=HM, bm=BM, event=event) except Exception as e: - logging.exception(f"{event.amt_assignment_id=}, {e=}") + LOG.exception(f"{event.amt_assignment_id=}, {e=}") emit_error_event( event_type="failed_process_assignment_submitted", amt_hit_type_id=event.amt_hit_type_id, ) - REDIS.xackdel(JB_EVENTS_STREAM, CONSUMER_GROUP, msg_id) - - -def handle_pending_msgs(): - # TODO!: This doesn't run at all. - - # Looks in the redis queue for msgs that - # are pending (read by a consumer but not ACK). These prob failed. - # Below is from chatgpt, idk if it works - pending = cast( - list[PendingEntry], - REDIS.xpending_range( - JB_EVENTS_STREAM, CONSUMER_GROUP, min="-", max="+", count=10 - ), - ) - - for entry in pending: - msg_id = entry["message_id"] - # Claim message if idle > 10 sec - if entry["idle"] > 10_000: # milliseconds - claimed = REDIS.xclaim( - JB_EVENTS_STREAM, - CONSUMER_GROUP, - CONSUMER_NAME, - min_idle_time=10_000, - message_ids=[msg_id], - ) - for cid, data in claimed: - msg_json = data["data"] - event = MTurkEvent.model_validate_json(msg_json) - if event.event_type == "AssignmentSubmitted": - # Try to process it again. If it fails, add - # it to the failed stream, so maybe we can fix - # and try again? - try: - process_assignment_submitted_event(event, cid) - REDIS.xack(JB_EVENTS_STREAM, CONSUMER_GROUP, cid) - except Exception as e: - logging.exception(e) - REDIS.xadd(JB_EVENTS_FAILED_STREAM, data) - REDIS.xack(JB_EVENTS_STREAM, CONSUMER_GROUP, cid) - else: - logging.info(f"Discarding {event}") - REDIS.xdel(JB_EVENTS_STREAM, msg_id) + redis_client.xackdel(JB_EVENTS_STREAM, CONSUMER_GROUP, msg_id) diff --git a/jb/flow/maintenance.py b/jb/flow/maintenance.py index f8ca971..4a2fe81 100644 --- a/jb/flow/maintenance.py +++ b/jb/flow/maintenance.py @@ -1,5 +1,3 @@ -from typing import Optional - from jb.decorators import HM from jb.flow.monitoring import emit_hit_event from jb.managers.amt import AMTManager @@ -10,7 +8,7 @@ def check_hit_status( amtm: AMTManager, amt_hit_id: str, amt_hit_type_id: str, - reason: Optional[str] = None, + reason: str | None = None, ) -> HitStatus: """ (this used to be called "process_hit") diff --git a/jb/flow/monitoring.py b/jb/flow/monitoring.py index 28f7271..22e9e93 100644 --- a/jb/flow/monitoring.py +++ b/jb/flow/monitoring.py @@ -1,12 +1,11 @@ import socket -from typing import Optional +from generalresearch.currency import USDCent from mypy_boto3_mturk.literals import EventTypeType from jb.config import settings from jb.decorators import influx_client -from generalresearchutils.currency import USDCent -from jb.models.definitions import HitStatus, AssignmentStatus +from jb.models.definitions import AssignmentStatus, HitStatus def write_hit_gauge(status: HitStatus, amt_hit_type_id: str, cnt: int) -> None: @@ -26,8 +25,6 @@ def write_hit_gauge(status: HitStatus, amt_hit_type_id: str, cnt: int) -> None: if influx_client: influx_client.write_points(points=[point]) - return None - def write_assignment_gauge( status: AssignmentStatus, amt_hit_type_id: str, cnt: int @@ -47,11 +44,9 @@ def write_assignment_gauge( if influx_client: influx_client.write_points(points=[point]) - return None - def emit_hit_event( - status: HitStatus, amt_hit_type_id: str, reason: Optional[str] = None + status: HitStatus, amt_hit_type_id: str, reason: str | None = None ) -> None: """ e.g. a HIT was created, Reviewable, etc. We don't have a "created" @@ -77,11 +72,9 @@ def emit_hit_event( if influx_client: influx_client.write_points([point]) - return None - def emit_assignment_event( - status: AssignmentStatus, amt_hit_type_id: str, reason: Optional[str] = None + status: AssignmentStatus, amt_hit_type_id: str, reason: str | None = None ) -> None: """ e.g. an Assignment was accepted/approved/reject @@ -106,8 +99,6 @@ def emit_assignment_event( if influx_client: influx_client.write_points([point]) - return None - def emit_mturk_notification_event( event_type: EventTypeType, amt_hit_type_id: str @@ -133,8 +124,6 @@ def emit_mturk_notification_event( if influx_client: influx_client.write_points([point]) - return None - def emit_error_event(event_type: str, amt_hit_type_id: str) -> None: """ @@ -157,8 +146,6 @@ def emit_error_event(event_type: str, amt_hit_type_id: str) -> None: if influx_client: influx_client.write_points([point]) - return None - def emit_bonus_event(amount: USDCent, amt_hit_type_id: str) -> None: """ @@ -179,5 +166,3 @@ def emit_bonus_event(amount: USDCent, amt_hit_type_id: str) -> None: if influx_client: influx_client.write_points([point]) - - return None diff --git a/jb/flow/setup_tasks.py b/jb/flow/setup_tasks.py index 4664374..f5cda48 100644 --- a/jb/flow/setup_tasks.py +++ b/jb/flow/setup_tasks.py @@ -1,6 +1,5 @@ -from jb.config import TOPIC_ARN, SUBSCRIPTION -from jb.decorators import SNS_CLIENT, AMT_CLIENT -from jb.config import settings +from jb.config import SUBSCRIPTION, TOPIC_ARN, settings +from jb.decorators import AMT_CLIENT, SNS_CLIENT def initial_setup(): diff --git a/jb/flow/tasks.py b/jb/flow/tasks.py index 77825d3..c555021 100644 --- a/jb/flow/tasks.py +++ b/jb/flow/tasks.py @@ -1,18 +1,13 @@ -import logging import time from typing import TypedDict, cast -from generalresearchutils.config import is_debug +from generalresearch.config import is_debug -from jb.decorators import AMTM, HTM, HM, HQM, pg_config +from jb.decorators import AMTM, HM, HQM, HTM, LOG, pg_config from jb.flow.maintenance import check_hit_status -from jb.flow.monitoring import write_hit_gauge, emit_hit_event +from jb.flow.monitoring import emit_hit_event, write_hit_gauge from jb.models.definitions import HitStatus -from jb.models.hit import HitType, HitQuestion, Hit - -logging.basicConfig() -logger = logging.getLogger() -logger.setLevel(logging.INFO) +from jb.models.hit import Hit, HitQuestion, HitType class HitRow(TypedDict): @@ -34,7 +29,7 @@ def check_stale_hits(): params={"status": HitStatus.Assignable.value}, ) for hit in cast(list[HitRow], res): - logging.info(f"check_stale_hits: {hit["amt_hit_id"]}") + LOG.info(f"check_stale_hits: {hit["amt_hit_id"]}") check_hit_status( amtm=AMTM, amt_hit_id=hit["amt_hit_id"], @@ -56,7 +51,7 @@ def check_expired_hits(): params={"status": HitStatus.Assignable.value}, ) for hit in cast(list[HitRow], res): - logging.info(f"check_expired_hits: {hit["amt_hit_id"]}") + LOG.info(f"check_expired_hits: {hit["amt_hit_id"]}") check_hit_status( amtm=AMTM, amt_hit_id=hit["amt_hit_id"], @@ -87,7 +82,7 @@ def refill_hits() -> None: assert hit_type.amt_hit_type_id active_count = HM.get_active_count(hit_type_id=hit_type.id) - logging.info( + LOG.info( f"HitType: {hit_type.amt_hit_type_id}, {hit_type.min_active=}, active_count={active_count}" ) write_hit_gauge( @@ -97,7 +92,7 @@ def refill_hits() -> None: ) if active_count < hit_type.min_active: cnt_todo = hit_type.min_active - active_count - logging.info(f"Refilling {cnt_todo} hits") + LOG.info(f"Refilling {cnt_todo} hits") for _ in range(cnt_todo): create_hit_from_hittype(hit_type) @@ -109,6 +104,6 @@ def refill_hits_task(): check_stale_hits() refill_hits() except Exception as e: - logging.exception(e) + LOG.exception(e) finally: time.sleep(5 * 60) @@ -1,14 +1,15 @@ from multiprocessing import Process -from typing import Any, Dict +from typing import Any from fastapi import FastAPI from fastapi.responses import HTMLResponse from starlette.middleware.cors import CORSMiddleware from starlette.middleware.trustedhost import TrustedHostMiddleware -from jb.views.common import common_router -from jb.settings import BASE_HTML from jb.config import settings +from jb.settings import BASE_HTML +from jb.views.auth import auth_router +from jb.views.common import common_router app = FastAPI( servers=[ @@ -21,6 +22,7 @@ app = FastAPI( version="1.0.0", ) + app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -31,12 +33,13 @@ app.add_middleware( app.add_middleware(TrustedHostMiddleware, allowed_hosts=["*"]) app.include_router(router=common_router) +app.include_router(router=auth_router) @app.get("/robots.txt") @app.get("/sitemap.xml") @app.get("/favicon.ico") -def return_nothing() -> Dict[str, Any]: +def return_nothing() -> dict[str, Any]: return {} @@ -52,7 +55,6 @@ def schedule_tasks(): from jb.flow.tasks import refill_hits_task Process(target=process_mturk_events_task).start() - # Process(target=handle_pending_msgs_task).start() Process(target=refill_hits_task).start() diff --git a/jb/managers/__init__.py b/jb/managers/__init__.py index e99569a..e69de29 100644 --- a/jb/managers/__init__.py +++ b/jb/managers/__init__.py @@ -1,23 +0,0 @@ -from enum import IntEnum -from typing import Collection - -from generalresearchutils.pg_helper import PostgresConfig - - -class Permission(IntEnum): - READ = 1 - UPDATE = 2 - CREATE = 3 - DELETE = 4 - - -class PostgresManager: - def __init__( - self, - pg_config: PostgresConfig, - permissions: Collection[Permission] = None, # type: ignore - **kwargs, # type: ignore - ): - super().__init__(**kwargs) - self.pg_config = pg_config - self.permissions = set(permissions) if permissions else set() diff --git a/jb/managers/amt.py b/jb/managers/amt.py index 88ae07d..17e5630 100644 --- a/jb/managers/amt.py +++ b/jb/managers/amt.py @@ -1,25 +1,24 @@ -import logging -from datetime import timezone, datetime -from typing import Tuple, Optional, List, Dict, Any +from datetime import datetime, timezone +from typing import Any import botocore.exceptions -from generalresearchutils.currency import USDCent +from generalresearch.currency import USDCent from mypy_boto3_mturk import MTurkClient from mypy_boto3_mturk.type_defs import ( AssignmentTypeDef, BonusPaymentTypeDef, CreateHITTypeResponseTypeDef, - GetHITResponseTypeDef, CreateHITWithHITTypeResponseTypeDef, + GetHITResponseTypeDef, ) from pydantic import ValidationError from jb.config import TOPIC_ARN -from jb.models import AMTAccount +from jb.models.amt import AMTAccount from jb.models.assignment import Assignment from jb.models.bonus import Bonus from jb.models.definitions import HitStatus -from jb.models.hit import HitType, HitQuestion, Hit +from jb.models.hit import Hit, HitQuestion, HitType REJECT_MESSAGE_UNKNOWN_ASSIGNMENT = "Unknown assignment" REJECT_MESSAGE_NO_WORK = "Assignment was submitted with no attempted work." @@ -56,7 +55,7 @@ class AMTManager: } ) - def get_hit_if_exists(self, amt_hit_id: str) -> Tuple[Optional[Hit], Optional[str]]: + def get_hit_if_exists(self, amt_hit_id: str) -> tuple[Hit | None, str | None]: try: res: GetHITResponseTypeDef = self.amt_client.get_hit(HITId=amt_hit_id) @@ -78,7 +77,7 @@ class AMTManager: return HitStatus.Disposed else: - logging.warning(msg) + LOG.warning(msg) return HitStatus.Unassignable return res.status @@ -137,7 +136,7 @@ class AMTManager: # Baddies have been known to submit assignments with purposely # malformed "answer" (xml) section, which will raise # a pydantic validation error. Try to parse again with no Answer. - logging.exception(e) + LOG.exception(e) ass_res["Answer"] = None # If it wasn't the Answer that caused the ValidationError, it'll raise again assignment = Assignment.from_amt_get_assignment(ass_res) @@ -146,13 +145,13 @@ class AMTManager: assert assignment.id is None return assignment - def get_assignment_if_exists(self, amt_assignment_id: str) -> Optional[Assignment]: + def get_assignment_if_exists(self, amt_assignment_id: str) -> Assignment | None: expected_err_msg = f"Assignment {amt_assignment_id} does not exist" try: return self.get_assignment(amt_assignment_id=amt_assignment_id) except botocore.exceptions.ClientError as e: - logging.warning(e) + LOG.warning(e) error_code = e.response["Error"]["Code"] error_msg = e.response["Error"]["Message"] if error_code == "RequestError" and expected_err_msg in error_msg: @@ -161,7 +160,7 @@ class AMTManager: def reject_assignment_if_possible( self, amt_assignment_id: str, msg: str = REJECT_MESSAGE_UNKNOWN_ASSIGNMENT - ) -> Optional[Dict[str, Any]]: + ) -> dict[str, Any] | None: # Unclear to me when this would fail try: @@ -170,7 +169,7 @@ class AMTManager: ) except botocore.exceptions.ClientError as e: - logging.warning(e) + LOG.warning(e) return None def approve_assignment_if_possible( @@ -178,7 +177,7 @@ class AMTManager: amt_assignment_id: str, msg: str = APPROVAL_MESSAGE, override_rejection: bool = False, - ) -> Optional[Dict[str, Any]]: + ) -> dict[str, Any] | None: # Unclear to me when this would fail try: @@ -189,7 +188,7 @@ class AMTManager: ) except botocore.exceptions.ClientError as e: - logging.warning(e) + LOG.warning(e) return None def update_hit_review_status(self, amt_hit_id: str, revert: bool = False) -> None: @@ -198,7 +197,7 @@ class AMTManager: self.amt_client.update_hit_review_status(HITId=amt_hit_id, Revert=revert) except botocore.exceptions.ClientError as e: - logging.warning(f"{amt_hit_id=}, {e}") + LOG.warning(f"{amt_hit_id=}, {e}") error_msg = e.response["Error"]["Message"] if "does not exist" in error_msg: @@ -207,8 +206,6 @@ class AMTManager: # elif "This HIT is currently in the state 'Reviewing'" in error_msg: # logging.warning(error_msg) - return None - def send_bonus( self, amt_worker_id: str, @@ -216,7 +213,7 @@ class AMTManager: amt_assignment_id: str, reason: str, unique_request_token: str, - ) -> Optional[Dict[str, Any]]: + ) -> dict[str, Any] | None: try: return self.amt_client.send_bonus( WorkerId=amt_worker_id, @@ -227,14 +224,12 @@ class AMTManager: ) except botocore.exceptions.ClientError as e: - logging.warning(f"{amt_worker_id=} {amt_assignment_id=}, {e}") + LOG.warning(f"{amt_worker_id=} {amt_assignment_id=}, {e}") return None - def get_bonus( - self, amt_assignment_id: str, payout_event_id: str - ) -> Optional[Bonus]: + def get_bonus(self, amt_assignment_id: str, payout_event_id: str) -> Bonus | None: - res: List[BonusPaymentTypeDef] = self.amt_client.list_bonus_payments( + res: list[BonusPaymentTypeDef] = self.amt_client.list_bonus_payments( AssignmentId=amt_assignment_id )["BonusPayments"] @@ -268,5 +263,3 @@ class AMTManager: self.amt_client.update_expiration_for_hit( HITId=hit["HITId"], ExpireAt=now ) - - return None diff --git a/jb/managers/assignment.py b/jb/managers/assignment.py index f6aa2ce..2740aee 100644 --- a/jb/managers/assignment.py +++ b/jb/managers/assignment.py @@ -1,11 +1,10 @@ from datetime import datetime, timezone -from typing import Optional from psycopg import sql from pydantic import NonNegativeInt, PositiveInt -from jb.managers import PostgresManager -from jb.models.assignment import AssignmentStub, Assignment +from jb.managers.base import PostgresManager +from jb.models.assignment import Assignment, AssignmentStub from jb.models.definitions import AssignmentStatus @@ -14,8 +13,7 @@ class AssignmentManager(PostgresManager): def create_stub(self, stub: AssignmentStub) -> None: assert stub.id is None data = stub.to_postgres() - query = sql.SQL( - """ + query = sql.SQL(""" INSERT INTO mtwerk_assignment (amt_assignment_id, amt_worker_id, status, created_at, modified_at, hit_id) @@ -23,16 +21,13 @@ class AssignmentManager(PostgresManager): (%(amt_assignment_id)s, %(amt_worker_id)s, %(status)s, %(created_at)s, %(modified_at)s, %(hit_id)s) RETURNING id; - """ - ) + """) - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - pk = c.fetchone()["id"] # type: ignore - conn.commit() + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + pk = c.fetchone()["id"] # type: ignore + conn.commit() stub.id = pk - return None def create(self, assignment: Assignment) -> None: # Typically this is NOT used (we'd create the stub when HIT is @@ -42,8 +37,7 @@ class AssignmentManager(PostgresManager): assert assignment.id is None data = assignment.to_postgres() - query = sql.SQL( - """ + query = sql.SQL(""" INSERT INTO mtwerk_assignment (amt_assignment_id, amt_worker_id, status, created_at, modified_at, hit_id, @@ -57,16 +51,13 @@ class AssignmentManager(PostgresManager): %(approval_time)s, %(rejection_time)s, %(requester_feedback)s, %(tsid)s) RETURNING id; - """ - ) + """) - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - pk = c.fetchone()["id"] # type: ignore - conn.commit() + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + pk = c.fetchone()["id"] # type: ignore + conn.commit() assignment.id = pk - return None def get_stub(self, amt_assignment_id: str) -> AssignmentStub: res = self.pg_config.execute_sql_query( @@ -85,7 +76,7 @@ class AssignmentManager(PostgresManager): assert len(res) == 1 return AssignmentStub.model_validate(res[0]) - def get_stub_if_exists(self, amt_assignment_id: str) -> Optional[AssignmentStub]: + def get_stub_if_exists(self, amt_assignment_id: str) -> AssignmentStub | None: try: return self.get_stub(amt_assignment_id=amt_assignment_id) except AssertionError: @@ -121,8 +112,7 @@ class AssignmentManager(PostgresManager): "amt_assignment_id": assignment.amt_assignment_id, "modified_at": now, } - query = sql.SQL( - """ + query = sql.SQL(""" UPDATE mtwerk_assignment SET submit_time = %(submit_time)s, auto_approval_time = %(auto_approval_time)s, @@ -130,15 +120,12 @@ class AssignmentManager(PostgresManager): tsid = %(tsid)s, modified_at = %(modified_at)s WHERE amt_assignment_id = %(amt_assignment_id)s - """ - ) + """) # We force this to fail if the assignment doesn't already exist in the db - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - assert c.rowcount == 1, f"Expected 1 row, got {c.rowcount}" - conn.commit() - return None + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + assert c.rowcount == 1, f"Expected 1 row, got {c.rowcount}" + conn.commit() def reject(self, assignment: Assignment) -> None: assert assignment.status == AssignmentStatus.Rejected @@ -156,8 +143,7 @@ class AssignmentManager(PostgresManager): "accept_time": assignment.accept_time, "modified_at": now, } - query = sql.SQL( - """ + query = sql.SQL(""" UPDATE mtwerk_assignment SET submit_time = %(submit_time)s, rejection_time = %(rejection_time)s, @@ -167,15 +153,12 @@ class AssignmentManager(PostgresManager): accept_time = %(accept_time)s, modified_at = %(modified_at)s WHERE amt_assignment_id = %(amt_assignment_id)s - """ - ) + """) # We force this to fail if the assignment doesn't already exist in the db - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - assert c.rowcount == 1, f"Expected 1 row, got {c.rowcount}" - conn.commit() - return None + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + assert c.rowcount == 1, f"Expected 1 row, got {c.rowcount}" + conn.commit() def approve(self, assignment: Assignment) -> None: assert assignment.status == AssignmentStatus.Approved @@ -194,8 +177,7 @@ class AssignmentManager(PostgresManager): "accept_time": assignment.accept_time, "modified_at": now, } - query = sql.SQL( - """ + query = sql.SQL(""" UPDATE mtwerk_assignment SET submit_time = %(submit_time)s, approval_time = %(approval_time)s, @@ -205,15 +187,12 @@ class AssignmentManager(PostgresManager): accept_time = %(accept_time)s, modified_at = %(modified_at)s WHERE amt_assignment_id = %(amt_assignment_id)s - """ - ) + """) # We force this to fail if the assignment doesn't already exist in the db - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - assert c.rowcount == 1, f"Expected 1 row, got {c.rowcount}" - conn.commit() - return None + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + assert c.rowcount == 1, f"Expected 1 row, got {c.rowcount}" + conn.commit() def missing_tsid_count( self, amt_worker_id: str, lookback_hrs: PositiveInt = 24 diff --git a/jb/managers/base.py b/jb/managers/base.py new file mode 100644 index 0000000..4b0637f --- /dev/null +++ b/jb/managers/base.py @@ -0,0 +1,16 @@ +from collections.abc import Collection + +from generalresearch.managers.base import Permission +from generalresearch.pg_helper import PostgresConfig + + +class PostgresManager: + def __init__( + self, + pg_config: PostgresConfig, + permissions: Collection[Permission] | None = None, + **kwargs, # type: ignore + ): + super().__init__(**kwargs) + self.pg_config = pg_config + self.permissions = set(permissions) if permissions else set() diff --git a/jb/managers/bonus.py b/jb/managers/bonus.py index 89b81f0..b649103 100644 --- a/jb/managers/bonus.py +++ b/jb/managers/bonus.py @@ -1,8 +1,8 @@ -from typing import List, Any +from typing import Any from psycopg import sql -from jb.managers import PostgresManager +from jb.managers.base import PostgresManager from jb.models.bonus import Bonus @@ -11,8 +11,7 @@ class BonusManager(PostgresManager): def create(self, bonus: Bonus) -> None: assert bonus.id is None data = bonus.to_postgres() - query = sql.SQL( - """ + query = sql.SQL(""" INSERT INTO mtwerk_bonus (payout_event_id, amt_worker_id, amount, grant_time, assignment_id, reason) VALUES ( @@ -29,20 +28,17 @@ class BonusManager(PostgresManager): %(reason)s ) RETURNING id, assignment_id; - """ - ) + """) - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - res = c.fetchone() - conn.commit() + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + res = c.fetchone() + conn.commit() bonus.id = res["id"] # type: ignore bonus.assignment_id = res["assignment_id"] # type: ignore - return None - def filter(self, amt_assignment_id: str) -> List[Bonus]: - res: List[Any] = self.pg_config.execute_sql_query( + def filter(self, amt_assignment_id: str) -> list[Bonus]: + res: list[Any] = self.pg_config.execute_sql_query( """ SELECT mb.*, ma.amt_assignment_id FROM mtwerk_bonus mb diff --git a/jb/managers/email_manager.py b/jb/managers/email_manager.py new file mode 100644 index 0000000..7298c60 --- /dev/null +++ b/jb/managers/email_manager.py @@ -0,0 +1,65 @@ +import requests +from generalresearch.config import is_debug + +from jb.config import settings + +MAUTIC_BASE_URL = "https://mail.jamesbillings67.com" +EMAIL_TEMPLATE_ID = 1 + +assert settings.mautic_api_key +auth_headers = {"Authorization": f"Basic {settings.mautic_api_key.get_secret_value()}"} + + +def get_or_create_contact(email: str, amt_worker_id: str | None = None) -> int: + body = {"email": email} + if amt_worker_id: + body["amt_worker_id"] = amt_worker_id + res = requests.post( + url=f"{MAUTIC_BASE_URL}/api/contacts/new", + json=body, + headers=auth_headers, + ).json() + return int(res["contact"]["id"]) + + +def send_login_email_from_url(mautic_url: str, magic_link: str) -> None: + if is_debug(): + print("MAGIC_LINK: ", magic_link) + return + + email_tokens = { + "magic_link": magic_link, + } + body = {"tokens": email_tokens} + response = requests.post(url=mautic_url, json=body, headers=auth_headers) + + try: + response.raise_for_status() + except requests.exceptions.HTTPError: + print(f"Failed to send email. Status code: {response.status_code}") + print(response.text) + raise + d = response.json() + + assert d.get("success"), f"Failed to send email: {d.get('failed')}" + print("Email sent successfully") + + +def send_login_email(email: str, magic_token: str) -> None: + contact_id = get_or_create_contact(email=email) + mautic_url = ( + f"{MAUTIC_BASE_URL}/api/emails/{EMAIL_TEMPLATE_ID}/contact/{contact_id}/send" + ) + magic_link = f"{settings.base_url}auth/magic-link/?token={magic_token}" + send_login_email_from_url(mautic_url, magic_link) + + +def send_amt_link_email(email: str, magic_token: str) -> None: + # Don't actually associate the email with the worker ID + # until they click the link + contact_id = get_or_create_contact(email=email) + mautic_url = ( + f"{MAUTIC_BASE_URL}/api/emails/{EMAIL_TEMPLATE_ID}/contact/{contact_id}/send" + ) + magic_link = f"{settings.base_url}auth/link-amt/?token={magic_token}" + send_login_email_from_url(mautic_url, magic_link) diff --git a/jb/managers/gr_api.py b/jb/managers/gr_api.py new file mode 100644 index 0000000..f0be1a7 --- /dev/null +++ b/jb/managers/gr_api.py @@ -0,0 +1,160 @@ +"""Client for General Research's product-user API.""" + +import logging +from typing import Any + +import requests + +from jb.models.auth import User + +logger = logging.getLogger("amtjb") + + +class GRApiError(RuntimeError): + """The General Research API could not satisfy a request.""" + + +class GRApiNotFoundError(GRApiError): + """The requested General Research API resource does not exist.""" + + +class GRApiManager: + def __init__( + self, + base_url: str, + token: str, + product_id: str, + timeout: float = 5.0, + session: requests.Session | None = None, + ) -> None: + if not token: + raise ValueError("General Research API token must not be empty") + + self.base_url = base_url.rstrip("/") + self.product_id = product_id + self.timeout = timeout + self.session = session or requests.Session() + self.session.headers.update( + { + "Authorization": token, + "Accept": "application/json", + "Content-Type": "application/json", + } + ) + + def _request(self, method: str, url: str, **kwargs: Any) -> requests.Response: + try: + response = self.session.request(method, url, timeout=self.timeout, **kwargs) + response.raise_for_status() + return response + except requests.HTTPError as exc: + if exc.response is not None and exc.response.status_code == 404: + raise GRApiNotFoundError( + f"General Research API resource not found: {method} {url}" + ) from exc + raise GRApiError( + f"General Research API request failed: {method} {url}" + ) from exc + except requests.RequestException as exc: + raise GRApiError( + f"General Research API request failed: {method} {url}" + ) from exc + + def _parse_user_response(self, res: dict[str, Any]) -> User: + return User.model_validate( + { + "product_user_id": res["product_user_id"], + "email": res["metadata"].get("email_address"), + "display_name": res["metadata"].get("display_name"), + "blocked": res["blocked"], + } + ) + + def ensure_user_exists(self, user: User) -> User: + """Idempotently create the product user if it does not already exist.""" + url = f"{self.base_url}/{self.product_id}/user/{user.product_user_id}/" + res = self._request("PUT", url).json() + if res["metadata"].get("email_address") is None: + self.set_user_email(user) + return self.get_user(user.product_user_id) + user_thl = self._parse_user_response(res) + if user_thl.email != user.email: + raise ValueError( + f"user {user.product_user_id} already exists with email {user_thl.email}" + ) + return user_thl + + def get_user(self, product_user_id: str) -> User: + """Retrieve the user's email address and display name.""" + url = f"{self.base_url}/{self.product_id}/user/{product_user_id}/" + res = self._request("GET", url).json() + return self._parse_user_response(res) + + def get_user_by_email(self, email: str) -> User: + user = User.model_validate({"email": email}) + return self.get_user(user.product_user_id) + + def set_user_email(self, user: User) -> None: + """This should only be called once per user upon account creation. + A user cannot change their email address.""" + url = f"{self.base_url}/{self.product_id}/user/{user.product_user_id}/metadata/" + _ = self._request( + "PATCH", + url, + json={"email_address": str(user.email)}, + ).json() + + def set_user_display_name(self, user: User) -> User: + """Can be called as many times as needed. Multiple + users can have the same display name.""" + if user.display_name is None: + return user + url = f"{self.base_url}/{self.product_id}/user/{user.product_user_id}/metadata/" + _ = self._request( + "PATCH", + url, + json={"display_name": user.display_name}, + ).json() + return self.get_user(user.product_user_id) + + def transition_user_from_amt(self, user: User, amt_worker_id: str) -> User: + """This should only be called once upon transition from an + AMT account to a General Research account.""" + url = f"{self.base_url}/{self.product_id}/user/{amt_worker_id}/" + try: + self._request("PATCH", url, json={"product_user_id": user.product_user_id}) + except GRApiNotFoundError as exc: + raise ValueError(f"User {amt_worker_id} does not exist") from exc + except GRApiError as exc: + http_error = exc.__cause__ + response = ( + http_error.response + if isinstance(http_error, requests.HTTPError) + else None + ) + if response is not None and response.status_code == 400: + try: + detail = response.json().get("detail") + except (ValueError, AttributeError): + detail = None + if detail == "Unable to update User": + raise ValueError( + "unable to update user, probably another user already " + "exists with this email" + ) from exc + raise + self.set_user_email(user) + transitioned_user = self.get_user(user.product_user_id) + logger.warning( + "Transitioned product user from AMT worker %s to %s with email %s", + amt_worker_id, + transitioned_user.product_user_id, + transitioned_user.email, + extra={ + "event": "transition_user_from_amt", + "amt_worker_id": amt_worker_id, + "product_user_id": transitioned_user.product_user_id, + "email": str(transitioned_user.email), + }, + ) + return transitioned_user diff --git a/jb/managers/hit.py b/jb/managers/hit.py index 63af2d4..bbae92b 100644 --- a/jb/managers/hit.py +++ b/jb/managers/hit.py @@ -1,11 +1,10 @@ from datetime import datetime, timezone -from typing import Optional, List from psycopg import sql -from jb.managers import PostgresManager +from jb.managers.base import PostgresManager from jb.models.definitions import HitStatus -from jb.models.hit import HitQuestion, HitType, Hit +from jb.models.hit import Hit, HitQuestion, HitType class HitQuestionManager(PostgresManager): @@ -13,21 +12,17 @@ class HitQuestionManager(PostgresManager): def create(self, question: HitQuestion) -> None: assert question.id is None data = question.to_postgres() - query = sql.SQL( - """ + query = sql.SQL(""" INSERT INTO mtwerk_question (url, height) VALUES (%(url)s, %(height)s) RETURNING id; - """ - ) + """) - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - pk = c.fetchone()["id"] # type: ignore - conn.commit() + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + pk = c.fetchone()["id"] # type: ignore + conn.commit() question.id = pk - return None def get_by_id(self, question_id: int) -> HitQuestion: res = self.pg_config.execute_sql_query( @@ -42,7 +37,7 @@ class HitQuestionManager(PostgresManager): assert len(res) == 1 return HitQuestion.model_validate(res[0]) - def get_by_values_if_exists(self, url: str, height: int) -> Optional[HitQuestion]: + def get_by_values_if_exists(self, url: str, height: int) -> HitQuestion | None: res = self.pg_config.execute_sql_query( """ SELECT * @@ -73,8 +68,7 @@ class HitTypeManager(PostgresManager): assert hit_type.amt_hit_type_id is not None data = hit_type.to_postgres() - query = sql.SQL( - """ + query = sql.SQL(""" INSERT INTO mtwerk_hittype ( amt_hit_type_id, title, @@ -96,27 +90,21 @@ class HitTypeManager(PostgresManager): %(min_active)s ) RETURNING id; - """ - ) + """) - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - pk = c.fetchone()["id"] # type: ignore - conn.commit() + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + pk = c.fetchone()["id"] # type: ignore + conn.commit() hit_type.id = pk - return None - - def filter_active(self) -> List[HitType]: - res = self.pg_config.execute_sql_query( - """ + def filter_active(self) -> list[HitType]: + res = self.pg_config.execute_sql_query(""" SELECT * FROM mtwerk_hittype WHERE min_active > 0 LIMIT 50 - """ - ) + """) if len(res) == 50: raise ValueError("Too many HitTypes!") @@ -135,7 +123,7 @@ class HitTypeManager(PostgresManager): assert len(res) == 1 return HitType.from_postgres(res[0]) - def get_if_exists(self, amt_hit_type_id: str) -> Optional[HitType]: + def get_if_exists(self, amt_hit_type_id: str) -> HitType | None: try: return self.get(amt_hit_type_id=amt_hit_type_id) except AssertionError: @@ -151,22 +139,18 @@ class HitTypeManager(PostgresManager): def set_min_active(self, hit_type: HitType) -> None: assert hit_type.id, "must be in the db first!" - query = sql.SQL( - """ + query = sql.SQL(""" UPDATE mtwerk_hittype SET min_active = %(min_active)s WHERE id = %(id)s - """ - ) + """) data = {"id": hit_type.id, "min_active": hit_type.min_active} - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - conn.commit() - row_cnt = c.rowcount + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + conn.commit() + row_cnt = c.rowcount assert row_cnt == 1, f"Expected 1 row updated, got {row_cnt}" - return None class HitManager(PostgresManager): @@ -175,8 +159,7 @@ class HitManager(PostgresManager): assert hit.amt_hit_id is not None assert hit.id is None data = hit.to_postgres() - query = sql.SQL( - """ + query = sql.SQL(""" INSERT INTO mtwerk_hit ( amt_hit_id, hit_type_id, @@ -210,38 +193,32 @@ class HitManager(PostgresManager): %(assignment_available_count)s ) RETURNING id; - """ - ) + """) - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - pk = c.fetchone()["id"] # type: ignore - conn.commit() + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + pk = c.fetchone()["id"] # type: ignore + conn.commit() hit.id = pk return hit def update_status(self, amt_hit_id: str, hit_status: HitStatus): now = datetime.now(tz=timezone.utc) - query = sql.SQL( - """ + query = sql.SQL(""" UPDATE mtwerk_hit SET status = %(status)s, modified_at = %(modified_at)s WHERE amt_hit_id = %(amt_hit_id)s; - """ - ) + """) data = { "amt_hit_id": amt_hit_id, "status": hit_status.value, "modified_at": now, } - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - conn.commit() - assert c.rowcount == 1, c.rowcount - return None + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + conn.commit() + assert c.rowcount == 1, c.rowcount def update_hit(self, hit: Hit): hit.modified_at = datetime.now(tz=timezone.utc) @@ -255,8 +232,7 @@ class HitManager(PostgresManager): "amt_hit_id", "modified_at", } - query = sql.SQL( - """ + query = sql.SQL(""" UPDATE mtwerk_hit SET status = %(status)s, review_status = %(review_status)s, assignment_pending_count = %(assignment_pending_count)s, @@ -265,17 +241,14 @@ class HitManager(PostgresManager): modified_at = %(modified_at)s WHERE amt_hit_id = %(amt_hit_id)s RETURNING id; - """ - ) + """) data = hit.model_dump(mode="json", include=fields) - with self.pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute(query, data) - conn.commit() - assert c.rowcount == 1, c.rowcount - hit.id = c.fetchone()["id"] # type: ignore - return None + with self.pg_config.make_connection() as conn, conn.cursor() as c: + c.execute(query, data) + conn.commit() + assert c.rowcount == 1, c.rowcount + hit.id = c.fetchone()["id"] # type: ignore def get_from_amt_id(self, amt_hit_id: str) -> Hit: res = self.pg_config.execute_sql_query( @@ -324,7 +297,7 @@ class HitManager(PostgresManager): return Hit.from_postgres(res) - def get_from_amt_id_if_exists(self, amt_hit_id: str) -> Optional[Hit]: + def get_from_amt_id_if_exists(self, amt_hit_id: str) -> Hit | None: try: return self.get_from_amt_id(amt_hit_id=amt_hit_id) @@ -333,7 +306,7 @@ class HitManager(PostgresManager): def get_active_count(self, hit_type_id: int) -> int: return self.pg_config.execute_sql_query( - """ + query=""" SELECT COUNT(1) as active_count FROM mtwerk_hit WHERE status = %(status)s diff --git a/jb/managers/thl.py b/jb/managers/thl.py index 6e8effc..85e0697 100644 --- a/jb/managers/thl.py +++ b/jb/managers/thl.py @@ -1,64 +1,18 @@ -from generalresearchutils.models.thl.payout import UserPayoutEvent -from generalresearchutils.models.thl.task_status import TaskStatusResponse -from generalresearchutils.models.thl.wallet.cashout_method import ( - CashoutRequestResponse, +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.task_status import TaskStatusResponse +from generalresearch.models.thl.wallet.cashout_method import ( CashoutRequestInfo, + CashoutRequestResponse, ) -from generalresearchutils.models.thl.user_profile import UserProfile -from generalresearchutils.currency import USDCent - from jb.config import settings +from jb.models.auth import User -from generalresearchutils.models.thl.definitions import PayoutStatus - - -from typing import Optional -import requests - -# TODO: Organize this more with other endpoints (offerwall, cashout -# requests/approvals, etc). - - -def get_user_profile(amt_worker_id: str) -> UserProfile: - url = f"{settings.fsb_host}{settings.product_id}/user/{amt_worker_id}/profile/" - res = requests.get(url).json() - - if res.get("detail") == "user not found": - raise ValueError("user not found") - user_profile = res["user_profile"] - # todo: these are computed fields, need a UserProfile parser - user_profile.pop("email_md5", None) - user_profile.pop("email_sha1", None) - user_profile.pop("email_sha256", None) - # todo: this contains computed fields inside each streak object - user_profile.pop("streaks", None) - # todo: this shouldn't be in here anyways ---v - user_profile["user"].pop("id", None) - - return UserProfile.model_validate(user_profile) - - -def get_user_blocked(amt_worker_id: str) -> bool: - # Not blocked if None - res = get_user_profile(amt_worker_id=amt_worker_id) - return res.user.blocked if res.user.blocked is not None else False - - -def get_user_blocked_or_not_exists(amt_worker_id: str) -> Optional[bool]: - try: - res = get_user_profile(amt_worker_id=amt_worker_id) - return res.user.blocked if res.user.blocked is not None else False - - except ValueError as e: - if e.args[0] == "user not found": - return True - - return None - - -def get_task_status(tsid: str) -> Optional[TaskStatusResponse]: +def get_task_status(tsid: str) -> TaskStatusResponse | None: url = f"{settings.fsb_host}{settings.product_id}/status/{tsid}/" d = requests.get(url).json() if d.get("msg") == "invalid tsid": @@ -68,18 +22,14 @@ def get_task_status(tsid: str) -> Optional[TaskStatusResponse]: def user_cashout_request( - amt_worker_id: str, amount: USDCent, cashout_method_id: str + user: User, amount: USDCent, cashout_method_id: str ) -> CashoutRequestInfo: - assert cashout_method_id in { - settings.amt_assignment_cashout_method, - settings.amt_bonus_cashout_method, - } assert isinstance(amount, USDCent) assert USDCent(0) < amount < USDCent(10_00) url = f"{settings.fsb_host}{settings.product_id}/cashout/" body: dict[str, str | int] = { - "bpuid": amt_worker_id, + "bpuid": user.product_user_id, "amount": int(amount), "cashout_method_id": cashout_method_id, } @@ -110,7 +60,7 @@ def get_wallet_balance(amt_worker_id: str) -> USDCent: return USDCent(requests.get(url, params=params).json()["wallet"]["amount"]) -def get_wallet_balance_if_non_negative(amt_worker_id: str) -> Optional[USDCent]: +def get_wallet_balance_if_non_negative(amt_worker_id: str) -> USDCent | None: url = f"{settings.fsb_host}{settings.product_id}/wallet/" params = {"bpuid": amt_worker_id} amt = requests.get(url, params=params).json()["wallet"]["amount"] diff --git a/jb/managers/worker.py b/jb/managers/worker.py index e2d7237..9e7d5e3 100644 --- a/jb/managers/worker.py +++ b/jb/managers/worker.py @@ -1,5 +1,3 @@ -from typing import List - from mypy_boto3_mturk.type_defs import WorkerBlockTypeDef from jb.decorators import AMT_CLIENT @@ -8,9 +6,9 @@ from jb.decorators import AMT_CLIENT class WorkerManager: @staticmethod - def fetch_worker_blocks() -> List[WorkerBlockTypeDef]: + def fetch_worker_blocks() -> list[WorkerBlockTypeDef]: p = AMT_CLIENT.get_paginator("list_worker_blocks") - res: List[WorkerBlockTypeDef] = [] + res: list[WorkerBlockTypeDef] = [] for item in p.paginate(): res.extend(item["WorkerBlocks"]) return res diff --git a/jb/models/__init__.py b/jb/models/__init__.py index 0aeae14..e69de29 100644 --- a/jb/models/__init__.py +++ b/jb/models/__init__.py @@ -1,40 +0,0 @@ -from decimal import Decimal -from typing import Optional - -from pydantic import BaseModel, Field, ConfigDict - - -class HTTPHeaders(BaseModel): - request_id: str = Field(alias="x-amzn-requestid", min_length=36, max_length=36) - content_type: str = Field(alias="content-type", min_length=26, max_length=26) - # 'content-length': '1255', - content_length: str = Field(alias="content-length", min_length=2) - # 'Mon, 15 Jan 2024 23:40:32 GMT' - date: str = Field() - - connection: Optional[str] = Field(default=None) # 'close' - - -class ResponseMetadata(BaseModel): - model_config = ConfigDict(extra="forbid", validate_assignment=True) - - request_id: str = Field(alias="RequestId", min_length=36, max_length=36) - status_code: int = Field(alias="HTTPStatusCode", ge=200, le=599) - headers: HTTPHeaders = Field(alias="HTTPHeaders") - retry_attempts: int = Field(alias="RetryAttempts", ge=0) - - -class AMTAccount(BaseModel): - model_config = ConfigDict(extra="ignore", validate_assignment=True) - - # Remaining available AWS Billing usage if you have enabled AWS Billing. - available_balance: Decimal = Field() - onhold_balance: Decimal = Field(default=Decimal(0)) - - # --- Properties --- - - @property - def is_healthy(self) -> bool: - # A healthy account is one with at least $2,500 worth of - # credit available to it - return self.available_balance >= 2_500 diff --git a/jb/models/amt.py b/jb/models/amt.py new file mode 100644 index 0000000..e012741 --- /dev/null +++ b/jb/models/amt.py @@ -0,0 +1,19 @@ +from decimal import Decimal + +from pydantic import BaseModel, ConfigDict, Field + + +class AMTAccount(BaseModel): + model_config = ConfigDict(extra="ignore", validate_assignment=True) + + # Remaining available AWS Billing usage if you have enabled AWS Billing. + available_balance: Decimal = Field() + onhold_balance: Decimal = Field(default=Decimal(0)) + + # --- Properties --- + + @property + def is_healthy(self) -> bool: + # A healthy account is one with at least $2,500 worth of + # credit available to it + return self.available_balance >= 2_500 diff --git a/jb/models/assignment.py b/jb/models/assignment.py index 92e5a89..fa6ccd5 100644 --- a/jb/models/assignment.py +++ b/jb/models/assignment.py @@ -1,23 +1,26 @@ import logging from datetime import datetime, timezone -from typing import Optional, TypedDict, Any +from typing import Any, TypedDict from xml.etree import ElementTree +from generalresearch.models.custom_types import AwareDatetimeISO, UUIDStr from mypy_boto3_mturk.type_defs import AssignmentTypeDef from pydantic import ( BaseModel, - Field, ConfigDict, - model_validator, + Field, PositiveInt, TypeAdapter, ValidationError, + model_validator, ) from typing_extensions import Self -from jb.models.custom_types import AMTBoto3ID, AwareDatetimeISO, UUIDStr +from jb.models.custom_types import AMTBoto3ID from jb.models.definitions import AssignmentStatus +logger = logging.getLogger("amtjb") + class AnswerDict(TypedDict): amt_assignment_id: str @@ -36,8 +39,8 @@ class AssignmentStub(BaseModel): validate_assignment=True, ) - id: Optional[PositiveInt] = Field(default=None) - hit_id: Optional[PositiveInt] = Field(default=None) + id: PositiveInt | None = Field(default=None) + hit_id: PositiveInt | None = Field(default=None) amt_assignment_id: AMTBoto3ID = Field() amt_hit_id: AMTBoto3ID = Field() amt_worker_id: str = Field(min_length=3, max_length=50) @@ -50,7 +53,7 @@ class AssignmentStub(BaseModel): description="When this record was saved in the database", ) - modified_at: Optional[AwareDatetimeISO] = Field( + modified_at: AwareDatetimeISO | None = Field( default_factory=lambda: datetime.now(tz=timezone.utc), description="When this record was updated / modified in the database", ) @@ -96,18 +99,18 @@ class Assignment(AssignmentStub): "submitted results.", ) - approval_time: Optional[AwareDatetimeISO] = Field( + approval_time: AwareDatetimeISO | None = Field( default=None, description="The date and time the Requester approved the results. This " "value is omitted from the assignment if the Requester has " "not yet approved the results.", ) - rejection_time: Optional[AwareDatetimeISO] = Field( + rejection_time: AwareDatetimeISO | None = Field( default=None, description="The date and time the Requester rejected the results.", ) - requester_feedback: Optional[str] = Field( + requester_feedback: str | None = Field( # Default: None. This field isn't returned with assignment data by # default. To request this field, specify a response group of # AssignmentFeedback. For information about response groups, see @@ -123,11 +126,11 @@ class Assignment(AssignmentStub): }, ) - answer_xml: Optional[str] = Field(default=None, exclude=True) + answer_xml: str | None = Field(default=None, exclude=True) # GRL Specific - tsid: Optional[UUIDStr] = Field(default=None) + tsid: UUIDStr | None = Field(default=None) # --- Validators --- @@ -140,7 +143,8 @@ class Assignment(AssignmentStub): values["tsid"] = TypeAdapter(UUIDStr).validate_python(tsid) except ValidationError as e: # Don't break the model validation if a baddie messes with the tsid in the answer. - logging.warning(e) + logger.warning(e) + values["tsid"] = None return values @@ -173,7 +177,7 @@ class Assignment(AssignmentStub): # --- Properties --- @property - def answers_dict(self) -> Optional[AnswerDict]: + def answers_dict(self) -> AnswerDict | None: # See https://docs.aws.amazon.com/AWSMechTurk/latest/AWSMturkAPI/ApiReference_AssignmentDataStructureArticle.html # https://docs.aws.amazon.com/AWSMechTurk/latest/AWSMechanicalTurkRequester/Concepts_NotificationsArticle.html if self.answer_xml is None: diff --git a/jb/models/auth.py b/jb/models/auth.py new file mode 100644 index 0000000..886a607 --- /dev/null +++ b/jb/models/auth.py @@ -0,0 +1,110 @@ +import hashlib +import hmac +from typing import Any + +from pydantic import ( + BaseModel, + ConfigDict, + EmailStr, + Field, + TypeAdapter, + computed_field, + field_validator, + model_validator, +) + +from jb.config import settings + + +def email_to_product_user_id(email: str) -> str: + """Return a deterministic, non-reversible product user ID for an email. + + The same normalized email and secret salt always produce the same ID. Keep + the salt private and stable; changing it changes every generated ID. + """ + assert settings.magic_token_salt + salt_bytes = settings.magic_token_salt.get_secret_value().encode("utf-8") + if len(salt_bytes) < 32: + raise ValueError("salt must be at least 32 bytes") + + if not email.isascii(): + raise ValueError("email must contain ASCII characters only") + + normalized_email = str(TypeAdapter(EmailStr).validate_python(email)).lower() + return hmac.new( + key=salt_bytes, + msg=normalized_email.encode("utf-8"), + digestmod=hashlib.sha1, + ).hexdigest() + + +class User(BaseModel): + """A user that has been authenticated and exists in THL""" + + email: EmailStr = Field() + + display_name: str | None = Field(default=None, max_length=255) + + blocked: bool = Field(default=False) + + @field_validator("email", mode="before") + @classmethod + def normalize_email(cls, value: Any) -> Any: + if not isinstance(value, str): + return value + if not value.isascii(): + raise ValueError("email must contain ASCII characters only") + return value.lower() + + @model_validator(mode="before") + @classmethod + def validate_product_user_id(cls, data: Any) -> Any: + if not isinstance(data, dict) or "product_user_id" not in data: + return data + + provided_id = data["product_user_id"] + email = data.get("email") + if email is None: + return data + + expected_id = email_to_product_user_id(str(email)) + if not isinstance(provided_id, str) or not hmac.compare_digest( + provided_id, expected_id + ): + raise ValueError( + f"product_user_id {provided_id} does not match email {email}" + ) + + # The computed field is authoritative; do not retain the input value. + validated_data = dict(data) + validated_data.pop("product_user_id") + return validated_data + + @computed_field + @property + def product_user_id(self) -> str: + return email_to_product_user_id(self.email) + + +class AccountLogin(BaseModel): + # There is no practical difference between an Account "login" and "create" + email: EmailStr = Field() + + +class MagicLinkExchangeRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + token: str = Field(min_length=1) + + +class AmtAccountLink(BaseModel): + model_config = ConfigDict(extra="forbid") + + email: EmailStr + amt_worker_id: str = Field(min_length=3, max_length=50) + + +class SessionResponse(BaseModel): + session_token: str + token_type: str = "bearer" + expires_in: int diff --git a/jb/models/bonus.py b/jb/models/bonus.py index a536dd1..c6da3c4 100644 --- a/jb/models/bonus.py +++ b/jb/models/bonus.py @@ -1,10 +1,11 @@ -from typing import Optional, Dict, Any +from typing import Any -from pydantic import BaseModel, Field, ConfigDict, PositiveInt +from generalresearch.currency import USDCent +from generalresearch.models.custom_types import AwareDatetimeISO, UUIDStr +from pydantic import BaseModel, ConfigDict, Field, PositiveInt from typing_extensions import Self -from generalresearchutils.currency import USDCent -from jb.models.custom_types import AMTBoto3ID, AwareDatetimeISO, UUIDStr +from jb.models.custom_types import AMTBoto3ID class Bonus(BaseModel): @@ -20,8 +21,8 @@ class Bonus(BaseModel): extra="forbid", validate_assignment=True, ) - id: Optional[PositiveInt] = Field(default=None) - assignment_id: Optional[PositiveInt] = Field(default=None) + id: PositiveInt | None = Field(default=None) + assignment_id: PositiveInt | None = Field(default=None) amt_worker_id: str = Field(min_length=3, max_length=50) amt_assignment_id: AMTBoto3ID = Field() @@ -40,7 +41,7 @@ class Bonus(BaseModel): return d @classmethod - def from_postgres(cls, data: Dict[str, Any]) -> Self: + def from_postgres(cls, data: dict[str, Any]) -> Self: data["amount"] = USDCent(round(data["amount"] * 100)) fields = set(cls.model_fields.keys()) data = {k: v for k, v in data.items() if k in fields} diff --git a/jb/models/custom_types.py b/jb/models/custom_types.py index 10bc9d1..385c1ba 100644 --- a/jb/models/custom_types.py +++ b/jb/models/custom_types.py @@ -1,101 +1,8 @@ import re -from datetime import datetime, timezone -from typing import Any, Optional -from uuid import UUID +from typing import Annotated -from pydantic import ( - AwareDatetime, - StringConstraints, - TypeAdapter, - HttpUrl, -) -from pydantic.functional_serializers import PlainSerializer -from pydantic.functional_validators import AfterValidator, BeforeValidator -from pydantic.networks import UrlConstraints -from pydantic_core import Url -from typing_extensions import Annotated - - -def convert_datetime_to_iso_8601_with_z_suffix(dt: datetime) -> str: - # By default, datetimes are serialized with the %f optional. We don't want that because - # then the deserialization fails if the datetime didn't have microseconds. - return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ") - - -def convert_str_dt(v: Any) -> Optional[AwareDatetime]: - # By default, pydantic is unable to handle tz-aware isoformat str. Attempt to parse a str - # that was dumped using the iso8601 format with Z suffix. - if v is not None and type(v) is str: - assert v.endswith("Z") and "T" in v, "invalid format" - return datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%fZ").replace( - tzinfo=timezone.utc - ) - return v - - -def assert_utc(v: AwareDatetime) -> AwareDatetime: - assert v.tzinfo == timezone.utc, "Timezone is not UTC" - return v - - -# Our custom AwareDatetime that correctly serializes and deserializes -# to an ISO8601 str with timezone -AwareDatetimeISO = Annotated[ - AwareDatetime, - BeforeValidator(convert_str_dt), - AfterValidator(assert_utc), - PlainSerializer( - lambda x: x.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), - when_used="json-unless-none", - ), -] - -# ISO 3166-1 alpha-2 (two-letter codes, lowercase) -# "Like" b/c it matches the format, but we're not explicitly checking -# it is one of our supported values. See models.thl.locales for that. -CountryISOLike = Annotated[ - str, StringConstraints(max_length=2, min_length=2, pattern=r"^[a-z]{2}$") -] -# 3-char ISO 639-2/B, lowercase -LanguageISOLike = Annotated[ - str, StringConstraints(max_length=3, min_length=3, pattern=r"^[a-z]{3}$") -] - - -def check_valid_uuid(v: str) -> str: - try: - assert UUID(v).hex == v - except Exception: - raise ValueError("Invalid UUID") - return v - - -# Our custom field that stores a UUID4 as the .hex string representation -UUIDStr = Annotated[ - str, - StringConstraints(min_length=32, max_length=32), - AfterValidator(check_valid_uuid), -] -# Accepts the non-hex representation and coerces -UUIDStrCoerce = Annotated[ - str, - StringConstraints(min_length=32, max_length=32), - BeforeValidator(lambda value: TypeAdapter(UUID).validate_python(value).hex), - AfterValidator(check_valid_uuid), -] - -# Same thing as UUIDStr with HttpUrl field. It is confusing that this -# is not a str https://github.com/pydantic/pydantic/discussions/6395 -HttpUrlStr = Annotated[ - str, - BeforeValidator(lambda value: str(TypeAdapter(HttpUrl).validate_python(value))), -] - -HttpsUrl = Annotated[Url, UrlConstraints(max_length=2083, allowed_schemes=["https"])] -HttpsUrlStr = Annotated[ - str, - BeforeValidator(lambda value: str(TypeAdapter(HttpsUrl).validate_python(value))), -] +from pydantic import StringConstraints +from pydantic.functional_validators import AfterValidator def check_valid_amt_boto3_id(v: str) -> str: diff --git a/jb/models/errors.py b/jb/models/errors.py index 94f5fbb..1fe71df 100644 --- a/jb/models/errors.py +++ b/jb/models/errors.py @@ -1,9 +1,9 @@ import re from enum import Enum -from pydantic import BaseModel, Field, ConfigDict, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator -from jb.models import ResponseMetadata +from jb.models.response import ResponseMetadata class BotoRequestErrorOperation(str, Enum): diff --git a/jb/models/event.py b/jb/models/event.py index f8867c0..fb5735b 100644 --- a/jb/models/event.py +++ b/jb/models/event.py @@ -1,9 +1,10 @@ -from typing import Dict, Any +from typing import Any +from generalresearch.models.custom_types import AwareDatetimeISO from mypy_boto3_mturk.literals import EventTypeType from pydantic import BaseModel, Field -from jb.models.custom_types import AwareDatetimeISO, AMTBoto3ID +from jb.models.custom_types import AMTBoto3ID class MTurkEvent(BaseModel): @@ -29,7 +30,7 @@ class MTurkEvent(BaseModel): ) @classmethod - def from_sns(cls, data: Dict[str, Any]): + def from_sns(cls, data: dict[str, Any]): return cls.model_validate( { "event_type": data["EventType"], diff --git a/jb/models/hit.py b/jb/models/hit.py index fba2ecf..a550943 100644 --- a/jb/models/hit.py +++ b/jb/models/hit.py @@ -1,25 +1,26 @@ -from datetime import datetime, timezone, timedelta -from typing import Optional, List, Dict, Any +from datetime import datetime, timedelta, timezone +from typing import Any from uuid import uuid4 from xml.etree import ElementTree +from generalresearch.currency import USDCent +from generalresearch.models.custom_types import AwareDatetimeISO, HttpsUrlStr from mypy_boto3_mturk.type_defs import HITTypeDef from pydantic import ( BaseModel, - Field, - PositiveInt, ConfigDict, + Field, NonNegativeInt, + PositiveInt, ) from typing_extensions import Self -from generalresearchutils.currency import USDCent -from jb.models.custom_types import AMTBoto3ID, HttpsUrlStr, AwareDatetimeISO -from jb.models.definitions import HitStatus, HitReviewStatus +from jb.models.custom_types import AMTBoto3ID +from jb.models.definitions import HitReviewStatus, HitStatus class HitQuestion(BaseModel): - id: Optional[PositiveInt] = Field(default=None) + id: PositiveInt | None = Field(default=None) url: HttpsUrlStr = Field() height: PositiveInt = Field(default=1_200, ge=100, le=4_000) @@ -33,7 +34,7 @@ class HitQuestion(BaseModel): def xml(self) -> str: return f"""<?xml version="1.0" encoding="UTF-8"?> <ExternalQuestion xmlns="http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2006-07-14/ExternalQuestion.xsd"> - <ExternalURL>{str(self.url)}</ExternalURL> + <ExternalURL>{self.url!s}</ExternalURL> <FrameHeight>{self.height}</FrameHeight> </ExternalQuestion>""" @@ -82,21 +83,25 @@ class HitType(HitTypeCommon): https://docs.aws.amazon.com/AWSMechTurk/latest/AWSMturkAPI/ApiReference_CreateHITTypeOperation.html """ - id: Optional[PositiveInt] = Field(default=None) - amt_hit_type_id: Optional[AMTBoto3ID] = Field(default=None) + id: PositiveInt | None = Field(default=None) + amt_hit_type_id: AMTBoto3ID | None = Field(default=None) # --- GRL Specific --- min_active: NonNegativeInt = Field(default=0, le=100_000) - def to_api_request_body(self): - return dict( - AutoApprovalDelayInSeconds=round(self.auto_approval_delay.total_seconds()), - AssignmentDurationInSeconds=round(self.assignment_duration.total_seconds()), - Reward=str(self.reward.to_usd()), - Title=self.title, - Keywords=self.keywords, - Description=self.description, - ) + def to_api_request_body(self) -> dict[str, Any]: + return { + "AutoApprovalDelayInSeconds": round( + self.auto_approval_delay.total_seconds() + ), + "AssignmentDurationInSeconds": round( + self.assignment_duration.total_seconds() + ), + "Reward": str(self.reward.to_usd()), + "Title": self.title, + "Keywords": self.keywords, + "Description": self.description, + } def to_postgres(self): d = self.model_dump(mode="json") @@ -104,12 +109,12 @@ class HitType(HitTypeCommon): return d @classmethod - def from_postgres(cls, data: Dict[str, Any]) -> Self: + def from_postgres(cls, data: dict[str, Any]) -> Self: data["reward"] = USDCent(round(data["reward"] * 100)) return cls.model_validate(data) - def generate_hit_amt_request(self, question: HitQuestion) -> Dict[str, Any]: - d = dict() + def generate_hit_amt_request(self, question: HitQuestion) -> dict[str, Any]: + d = {} d["HITTypeId"] = self.amt_hit_type_id d["MaxAssignments"] = 1 d["LifetimeInSeconds"] = round(timedelta(days=14).total_seconds()) @@ -124,9 +129,9 @@ class Hit(HitTypeCommon): validate_assignment=True, ) - id: Optional[PositiveInt] = Field(default=None) - hit_type_id: Optional[PositiveInt] = Field(default=None) - question_id: Optional[PositiveInt] = Field(default=None) + id: PositiveInt | None = Field(default=None) + hit_type_id: PositiveInt | None = Field(default=None) + question_id: PositiveInt | None = Field(default=None) amt_hit_id: AMTBoto3ID = Field() amt_hit_type_id: AMTBoto3ID = Field() @@ -138,10 +143,8 @@ class Hit(HitTypeCommon): # TODO: Check if this is actually ever going to be None. I type fixed it, # but I don't have anything to suggest it isn't requred. -- Max 2026-02-24 - creation_time: Optional[AwareDatetimeISO] = Field( - default=None, description="From aws" - ) - expiration: Optional[AwareDatetimeISO] = Field(default=None) + creation_time: AwareDatetimeISO | None = Field(default=None, description="From aws") + expiration: AwareDatetimeISO | None = Field(default=None) # GRL Specific created_at: AwareDatetimeISO = Field( @@ -155,7 +158,7 @@ class Hit(HitTypeCommon): # -- Hit specific - qualification_requirements: Optional[List[Dict[str, Any]]] = Field(default=None) + qualification_requirements: list[dict[str, Any]] | None = Field(default=None) max_assignments: int = Field() # # this comes back as expiration. only for the request @@ -177,27 +180,27 @@ class Hit(HitTypeCommon): assert hit_type.amt_hit_type_id is not None h = cls.model_validate( - dict( - amt_hit_id=data["HITId"], - amt_hit_type_id=data["HITTypeId"], - amt_group_id=data["HITGroupId"], - status=HitStatus[data["HITStatus"]], - review_status=HitReviewStatus[data["HITReviewStatus"]], - creation_time=data["CreationTime"].astimezone(tz=timezone.utc), - expiration=data["Expiration"].astimezone(tz=timezone.utc), - hit_question_xml=data["Question"], - qualification_requirements=data["QualificationRequirements"], - max_assignments=data["MaxAssignments"], - assignment_pending_count=data["NumberOfAssignmentsPending"], - assignment_available_count=data["NumberOfAssignmentsAvailable"], - assignment_completed_count=data["NumberOfAssignmentsCompleted"], - description=data["Description"], - keywords=data["Keywords"], - reward=USDCent(round(float(data["Reward"]) * 100)), - title=data["Title"], - question_id=question.id, - hit_type_id=hit_type.id, - ) + { + "amt_hit_id": data["HITId"], + "amt_hit_type_id": data["HITTypeId"], + "amt_group_id": data["HITGroupId"], + "status": HitStatus[data["HITStatus"]], + "review_status": HitReviewStatus[data["HITReviewStatus"]], + "creation_time": data["CreationTime"].astimezone(tz=timezone.utc), + "expiration": data["Expiration"].astimezone(tz=timezone.utc), + "hit_question_xml": data["Question"], + "qualification_requirements": data["QualificationRequirements"], + "max_assignments": data["MaxAssignments"], + "assignment_pending_count": data["NumberOfAssignmentsPending"], + "assignment_available_count": data["NumberOfAssignmentsAvailable"], + "assignment_completed_count": data["NumberOfAssignmentsCompleted"], + "description": data["Description"], + "keywords": data["Keywords"], + "reward": USDCent(round(float(data["Reward"]) * 100)), + "title": data["Title"], + "question_id": question.id, + "hit_type_id": hit_type.id, + } ) return h @@ -205,27 +208,27 @@ class Hit(HitTypeCommon): @classmethod def from_amt_get_hit(cls, data: HITTypeDef) -> Self: h = cls.model_validate( - dict( - amt_hit_id=data["HITId"], - amt_hit_type_id=data["HITTypeId"], - amt_group_id=data["HITGroupId"], - status=HitStatus[data["HITStatus"]], - review_status=HitReviewStatus[data["HITReviewStatus"]], - creation_time=data["CreationTime"].astimezone(tz=timezone.utc), - expiration=data["Expiration"].astimezone(tz=timezone.utc), - hit_question_xml=data["Question"], - qualification_requirements=data["QualificationRequirements"], - max_assignments=data["MaxAssignments"], - assignment_pending_count=data["NumberOfAssignmentsPending"], - assignment_available_count=data["NumberOfAssignmentsAvailable"], - assignment_completed_count=data["NumberOfAssignmentsCompleted"], - description=data["Description"], - keywords=data["Keywords"], - reward=USDCent(round(float(data["Reward"]) * 100)), - title=data["Title"], - question_id=None, - hit_type_id=None, - ) + { + "amt_hit_id": data["HITId"], + "amt_hit_type_id": data["HITTypeId"], + "amt_group_id": data["HITGroupId"], + "status": HitStatus[data["HITStatus"]], + "review_status": HitReviewStatus[data["HITReviewStatus"]], + "creation_time": data["CreationTime"].astimezone(tz=timezone.utc), + "expiration": data["Expiration"].astimezone(tz=timezone.utc), + "hit_question_xml": data["Question"], + "qualification_requirements": data["QualificationRequirements"], + "max_assignments": data["MaxAssignments"], + "assignment_pending_count": data["NumberOfAssignmentsPending"], + "assignment_available_count": data["NumberOfAssignmentsAvailable"], + "assignment_completed_count": data["NumberOfAssignmentsCompleted"], + "description": data["Description"], + "keywords": data["Keywords"], + "reward": USDCent(round(float(data["Reward"]) * 100)), + "title": data["Title"], + "question_id": None, + "hit_type_id": None, + } ) return h @@ -235,7 +238,7 @@ class Hit(HitTypeCommon): return d @classmethod - def from_postgres(cls, data: Dict[str, Any]) -> Self: + def from_postgres(cls, data: dict[str, Any]) -> Self: data["reward"] = USDCent(round(data["reward"] * 100)) return cls.model_validate(data) @@ -248,7 +251,7 @@ class Hit(HitTypeCommon): } res = {} - lookup_table = dict(ExternalURL="url", FrameHeight="height") + lookup_table = {"ExternalURL": "url", "FrameHeight": "height"} for a in root.findall("mt:*", ns): key = lookup_table[a.tag.split("}")[1]] val = a.text diff --git a/jb/models/response.py b/jb/models/response.py new file mode 100644 index 0000000..22985af --- /dev/null +++ b/jb/models/response.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel, ConfigDict, Field + + +class HTTPHeaders(BaseModel): + request_id: str = Field(alias="x-amzn-requestid", min_length=36, max_length=36) + content_type: str = Field(alias="content-type", min_length=26, max_length=26) + # 'content-length': '1255', + content_length: str = Field(alias="content-length", min_length=2) + # 'Mon, 15 Jan 2024 23:40:32 GMT' + date: str = Field() + + connection: str | None = Field(default=None) # 'close' + + +class ResponseMetadata(BaseModel): + model_config = ConfigDict(extra="forbid", validate_assignment=True) + + request_id: str = Field(alias="RequestId", min_length=36, max_length=36) + status_code: int = Field(alias="HTTPStatusCode", ge=200, le=599) + headers: HTTPHeaders = Field(alias="HTTPHeaders") + retry_attempts: int = Field(alias="RetryAttempts", ge=0) diff --git a/jb/settings.py b/jb/settings.py index d529591..1c02075 100644 --- a/jb/settings.py +++ b/jb/settings.py @@ -1,78 +1,108 @@ -import os from functools import lru_cache +from os.path import abspath +from os.path import dirname as pdirname +from os.path import join as pjoin from pathlib import Path -from typing import Optional -from generalresearchutils.models.custom_types import InfluxDsn -from pydantic import Field, PostgresDsn, HttpUrl, RedisDsn -from pydantic_settings import BaseSettings, SettingsConfigDict +from generalresearch.config import GRLBaseSettings, is_debug +from generalresearch.models.custom_types import ( + InfluxDsn, + UUIDStr, +) +from pydantic import Field, HttpUrl, PostgresDsn, SecretStr, model_validator +from pydantic_settings import SettingsConfigDict -from jb.models.custom_types import UUIDStr - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = pdirname(pdirname(abspath(__file__))) BASE_HTML_PATH = Path(BASE_DIR) / "templates" / "base.html" BASE_HTML = BASE_HTML_PATH.read_text() -class AmtJbBaseSettings(BaseSettings): - debug: bool = Field(default=True) - - redis: Optional[RedisDsn] = Field(default=None) - redis_timeout: float = Field(default=0.10) - - amt_jb_db: PostgresDsn = Field() - - amt_endpoint: Optional[HttpUrl] = Field(default=None) - amt_access_id: Optional[str] = Field(default=None) - amt_secret_key: Optional[str] = Field(default=None) - - aws_owner_id: str = Field() - aws_subscription_arn: str = Field() +class Settings(GRLBaseSettings): - amt_bonus_cashout_method: str = Field() - amt_assignment_cashout_method: str = Field() - - -class Settings(AmtJbBaseSettings): model_config = SettingsConfigDict( - env_prefix="", + env_file=(".env.test", ".env.testing", ".env.staging", ".env.prod"), + env_file_encoding="utf-8", case_sensitive=False, - env_file=os.path.join(BASE_DIR, ".env"), extra="allow", cli_parse_args=False, ) - debug: bool = False + + amt_jb_db: PostgresDsn | None = Field(default=None) + + amt_endpoint: HttpUrl | None = Field(default=None) + amt_access_id: str | None = Field(default=None) + amt_secret_key: str | None = Field(default=None) + + aws_owner_id: str | None = Field(default=None) + aws_subscription_arn: str | None = Field(default=None) + + # --- Pytest --- + app_name: str = "AMT JB API" + base_url: HttpUrl = Field(default=HttpUrl("https://jamesbillings67.com/")) fsb_host: HttpUrl = Field(default=HttpUrl("https://fsb.generalresearch.com/")) # Needed for admin function on fsb w/o authentication - fsb_host_private_route: Optional[str] = Field(default=None) + fsb_host_private_route: str | None = Field(default=None) - product_id: UUIDStr = Field() + product_id: UUIDStr | None = Field(default=None) - influx_db: Optional[InfluxDsn] = Field(default=None) + influx_db: InfluxDsn | None = Field(default=None) - sns_path: str = Field() + sns_path: str | None = Field(default=None) + session_token_ttl_seconds: int = Field(default=30 * 24 * 60 * 60, gt=0) # 30 days + session_jwt_secret: SecretStr | None = Field(default=None, min_length=32) -class TestSettings(Settings): - model_config = SettingsConfigDict( - env_prefix="", - case_sensitive=False, - env_file=os.path.join(BASE_DIR, ".env.test"), - extra="allow", - cli_parse_args=False, - ) - debug: bool = True - app_name: str = "AMT JB API Test" + magic_token_salt: SecretStr | None = Field(default=None, min_length=32) + gr_api_host: HttpUrl = Field(default=HttpUrl("https://generalresearch.com/api/v2/")) + gr_api_token: SecretStr | None = Field(default=None, min_length=1) -@lru_cache -def get_settings(): - return Settings() + mautic_api_key: SecretStr | None = Field(default=None, min_length=32) + + @model_validator(mode="after") + def validate_host_and_key(self) -> "Settings": + + self.debug = is_debug() + + if self.debug: + return self + + if not self.amt_jb_db: + raise ValueError("amt_jb_db is required") + + if not self.aws_owner_id: + raise ValueError("aws_owner_id is required") + + if not self.aws_subscription_arn: + raise ValueError("aws_subscription_arn is required") + + if not self.product_id: + raise ValueError("product_id is required") + + if not self.sns_path: + raise ValueError("sns_path is required") + + if not self.session_jwt_secret: + raise ValueError("session_jwt_secret is required") + + if not self.magic_token_salt: + raise ValueError("magic_token_salt is required") + + if self.session_jwt_secret == self.magic_token_salt: + raise ValueError("JWT Secret must be different than Magic Token Salt") + + if not self.gr_api_token: + raise ValueError("gr_api_token is required") + + if not self.mautic_api_key: + raise ValueError("mautic_api_key is required") + + return self @lru_cache -def get_test_settings(): - return TestSettings() +def get_settings(): + return Settings() diff --git a/jb/views/auth.py b/jb/views/auth.py new file mode 100644 index 0000000..ce9c1e0 --- /dev/null +++ b/jb/views/auth.py @@ -0,0 +1,203 @@ +from typing import Annotated +from urllib.parse import urlencode + +from fastapi import APIRouter, Depends, HTTPException, Response, status +from fastapi.responses import HTMLResponse, RedirectResponse + +from jb.api.auth import ( + SESSION_COOKIE_NAME, + create_session, + get_authenticated_user, +) +from jb.api.magic_token import ( + consume_amt_account_link_token, + consume_magic_token, + create_amt_account_link_token, + create_magic_token, +) +from jb.config import settings +from jb.decorators import LOG +from jb.dependencies import get_gr_api_manager +from jb.managers.email_manager import ( + get_or_create_contact, + send_amt_link_email, + send_login_email, +) +from jb.managers.gr_api import GRApiManager +from jb.models.auth import ( + AccountLogin, + AmtAccountLink, + MagicLinkExchangeRequest, + User, +) +from jb.settings import BASE_HTML + +auth_router = APIRouter(prefix="/auth", tags=["Auth"]) + + +@auth_router.post("/magic-link/request") +def request_magic_link(body: AccountLogin) -> dict[str, str]: + """Create a magic link.""" + email = str(body.email) + token = create_magic_token(user_email=email) + + if settings.debug: + query = urlencode({"token": token}) + return {"magic_link": f"{settings.base_url}auth/magic-link/?{query}"} + + send_login_email(email=email, magic_token=token) + return {"detail": "Link sent. Check your inbox and follow the link to log in."} + + +@auth_router.get("/magic-link/", response_class=HTMLResponse, include_in_schema=False) +def magic_link_landing_page( + gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], + token: str | None = None, +) -> Response: + """Serve the SPA without redeeming the token; email prefetches are harmless.""" + if settings.debug: + if token is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="token is required", + ) + response = RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER) + _exchange_magic_link(token, response, gr_api) + return response + return HTMLResponse( + BASE_HTML, + headers={ + "Cache-Control": "no-store", + "Referrer-Policy": "no-referrer", + "X-Robots-Tag": "noindex, nofollow", + }, + ) + + +@auth_router.post("/magic-link/exchange", status_code=status.HTTP_204_NO_CONTENT) +def exchange_magic_link( + body: MagicLinkExchangeRequest, + response: Response, + gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], +) -> None: + """Exchange a magic link only after its landing page makes an explicit POST.""" + _exchange_magic_link(body.token, response, gr_api) + + +def _exchange_magic_link(token: str, response: Response, gr_api: GRApiManager) -> None: + user_email = consume_magic_token(token) + user = gr_api.ensure_user_exists(User.model_validate({"email": user_email})) + response.set_cookie( + key=SESSION_COOKIE_NAME, + value=create_session(user.product_user_id), + max_age=settings.session_token_ttl_seconds, + httponly=True, + secure=not settings.debug, + samesite="lax", + path="/", + ) + + +@auth_router.post("/link-amt/request") +def link_amt_account(body: AmtAccountLink) -> dict[str, str]: + """Link an AMT account and login.""" + email = str(body.email) + amt_worker_id = body.amt_worker_id + + # TODO! Prevent a user that's already transitioned their account, from being + # TODO! able to continuously create this special link token. + # TODO! Max Notes: this seems to be handled within the gr-api, and that + # TODO! can raise, the following line would / should fail if needed. + + token = create_amt_account_link_token(email=email, amt_worker_id=amt_worker_id) + + if settings.debug: + query = urlencode({"token": token}) + return {"magic_link": f"{settings.base_url}auth/link-amt/?{query}"} + + send_amt_link_email(email=email, magic_token=token) + return {} + + +@auth_router.get("/debug/", response_class=HTMLResponse, include_in_schema=False) +def link_amt_account_landing_page( + gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], + token: str | None = None, +) -> HTMLResponse: + """Serve the account-link SPA without consuming the one-time token.""" + + # TODO! Try catch any of this, and if it fails, show the user a + # TODO! failed HTML page. As of now, it shows them a failed JSON response. + + if settings.debug: + if token is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="token is required", + ) + + _response = RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER) + _exchange_amt_account_link(token=token, response=_response, gr_api=gr_api) + + return HTMLResponse( + BASE_HTML, + headers={ + "Cache-Control": "no-store", + "Referrer-Policy": "no-referrer", + "X-Robots-Tag": "noindex, nofollow", + }, + ) + + +@auth_router.post("/link-amt/exchange", status_code=status.HTTP_204_NO_CONTENT) +def exchange_amt_account_link( + body: MagicLinkExchangeRequest, + response: Response, + gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], +) -> None: + """Validate the email link, then transition the bound AMT account.""" + try: + _exchange_amt_account_link(body.token, response, gr_api) + except ValueError as e: + LOG.error(f"Failed to exchange AMT account link: {e}") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +def _exchange_amt_account_link(token: str, response: Response, gr_api: GRApiManager): + token_data = consume_amt_account_link_token(token) + email = token_data.email + amt_worker_id = token_data.amt_worker_id + + user = User(email=email) + user = gr_api.transition_user_from_amt(user=user, amt_worker_id=amt_worker_id) + + # In Mautic, associate the email with the worker ID (AFTER the user has transitioned) + get_or_create_contact(email=email, amt_worker_id=amt_worker_id) + + session_token = create_session(user.product_user_id) + response.set_cookie( + key=SESSION_COOKIE_NAME, + value=session_token, + max_age=settings.session_token_ttl_seconds, + httponly=True, + secure=not settings.debug, + samesite="lax", + path="/", + ) + + +@auth_router.get("/session", response_model=User) +def get_session( + user: Annotated[User, Depends(get_authenticated_user)], +) -> User: + return user + + +@auth_router.delete("/session", status_code=status.HTTP_204_NO_CONTENT) +def delete_session( + response: Response, +) -> None: + # Logout is idempotent so clients can always discard their local token. + # JWT sessions are stateless, so logout discards the browser cookie. A token + # copied elsewhere remains valid until its short, configured expiration. + response.delete_cookie(key=SESSION_COOKIE_NAME, path="/") diff --git a/jb/views/common.py b/jb/views/common.py index 7011557..9eee453 100644 --- a/jb/views/common.py +++ b/jb/views/common.py @@ -1,19 +1,19 @@ import json -from typing import Dict, Any +from typing import Annotated, Any import requests -from fastapi import Request, APIRouter, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import HTMLResponse +from generalresearch.redis_helper import RedisConfig from starlette.responses import RedirectResponse -from jb.config import settings, JB_EVENTS_STREAM -from jb.decorators import REDIS, HM -from jb.flow.monitoring import emit_assignment_event, emit_mturk_notification_event -from jb.models.definitions import AssignmentStatus +from jb.api.auth import get_authenticated_user +from jb.config import JB_EVENTS_STREAM, settings +from jb.decorators import get_redis_config +from jb.flow.monitoring import emit_mturk_notification_event +from jb.models.auth import User from jb.models.event import MTurkEvent from jb.settings import BASE_HTML -from jb.config import settings -from jb.views.tasks import process_request common_router = APIRouter(prefix="", tags=["API"], include_in_schema=True) @@ -29,37 +29,24 @@ async def work(request: Request): amt_hit_id = request.query_params.get("hitId", None) print(f"work: {amt_assignment_id=} {worker_id=} {amt_hit_id=}") - if not worker_id: + if ( + not worker_id + or not amt_assignment_id + or amt_assignment_id == "ASSIGNMENT_ID_NOT_AVAILABLE" + ): return RedirectResponse( url=f"/preview/?{request.url.query}" if request.url.query else "/preview/", status_code=302, ) - if amt_assignment_id is None or amt_assignment_id == "ASSIGNMENT_ID_NOT_AVAILABLE": - # Worker is previewing the HIT - amt_hit_type_id = "unknown" - if amt_hit_id: - hit = HM.get_from_amt_id(amt_hit_id=amt_hit_id) - amt_hit_type_id = hit.amt_hit_type_id - emit_assignment_event( - status=AssignmentStatus.PreviewState, amt_hit_type_id=amt_hit_type_id - ) - return RedirectResponse( - url=f"/preview/?{request.url.query}" if request.url.query else "/preview/", - status_code=302, - ) + return HTMLResponse(BASE_HTML) - try: - # The Worker has accepted the HIT - process_request(request) - except Exception: - raise HTTPException(status_code=500, detail="Error processing request") - return HTMLResponse(BASE_HTML) +RedisConfigDep = Annotated[RedisConfig, Depends(get_redis_config)] @common_router.post(path=f"/{settings.sns_path}/", include_in_schema=False) -async def mturk_notifications(request: Request): +async def mturk_notifications(request: Request, redis_config: RedisConfigDep): """ Our SNS topic will POST to this endpoint whenever we get a new message """ @@ -77,7 +64,7 @@ async def mturk_notifications(request: Request): case "Notification": msg = json.loads(message["Message"]) print("Received MTurk event:", msg) - enqueue_mturk_notifications(msg) + enqueue_mturk_notifications(msg=msg, redis_config=redis_config) case _: raise HTTPException(status_code=500, detail="Invalid JSON") @@ -85,10 +72,27 @@ async def mturk_notifications(request: Request): return {"status": "ok"} -def enqueue_mturk_notifications(msg: Dict[str, Any]) -> None: +def enqueue_mturk_notifications(msg: dict[str, Any], redis_config: RedisConfig) -> None: + redis_client = redis_config.create_redis_client() + for evt in msg["Events"]: event = MTurkEvent.from_sns(evt) emit_mturk_notification_event( event_type=event.event_type, amt_hit_type_id=event.amt_hit_type_id ) - REDIS.xadd(JB_EVENTS_STREAM, {"data": event.model_dump_json()}) + + print("enqueue_mturk_notifications", event.model_dump_json()) + redis_client.xadd(JB_EVENTS_STREAM, {"data": event.model_dump_json()}) + + +@common_router.get(path="/work/direct/", response_class=HTMLResponse) +async def work_direct( + request: Request, + user: Annotated[User, Depends(get_authenticated_user)], +): + """ + View for direct work (not on AMT). + Makes sure user is authenticated. + """ + # todo: emit event + return HTMLResponse(BASE_HTML) diff --git a/jb/views/tasks.py b/jb/views/tasks.py deleted file mode 100644 index 15857c3..0000000 --- a/jb/views/tasks.py +++ /dev/null @@ -1,78 +0,0 @@ -from datetime import datetime, timezone, timedelta - -from fastapi import Request - -from jb.decorators import AMTM, AM, HM -from jb.flow.maintenance import check_hit_status -from jb.flow.monitoring import emit_assignment_event -from jb.models.assignment import AssignmentStub -from jb.models.definitions import AssignmentStatus - - -def process_request(request: Request) -> None: - """ - A worker has loaded the HIT (work) page and (probably) accepted the HIT. - AMT creates an assignment, tied to this hit and this worker. - Create it in the DB. - """ - amt_assignment_id = request.query_params.get("assignmentId", None) - if amt_assignment_id == "ASSIGNMENT_ID_NOT_AVAILABLE": - raise ValueError("shouldn't happen") - - amt_hit_id = request.query_params.get("hitId", None) - amt_worker_id = request.query_params.get("workerId", None) - print(f"process_request: {amt_assignment_id=} {amt_worker_id=} {amt_hit_id=}") - assert amt_worker_id and amt_hit_id and amt_assignment_id - - # Check that the HIT is still valid - hit = HM.get_from_amt_id_if_exists(amt_hit_id=amt_hit_id) - if not hit: - raise ValueError(f"Hit {amt_hit_id} not found in DB") - - _ = check_hit_status( - amtm=AMTM, amt_hit_id=amt_hit_id, amt_hit_type_id=hit.amt_hit_type_id - ) - - emit_assignment_event( - status=AssignmentStatus.Accepted, - amt_hit_type_id=hit.amt_hit_type_id, - ) - - # I think it won't be assignable anymore? idk - # assert hit_status == HitStatus.Assignable, f"hit {amt_hit_id} {hit_status=}. Expected Assignable" - - # I would like to verify in the AMT API that this assignment is valid, but there - # is no way to do that (until the assignment is submitted) - - # # Make an offerwall to create a user account... - # # todo: GSS: Do we really need to do this??? - # client_ip = get_client_ip(request) - # url = f"{settings.fsb_host}{settings.product_id}/offerwall/45b7228a7/" - # _ = requests.get( - # url, - # {"bpuid": amt_worker_id, "ip": client_ip, "n_bins": 1, "format": "json"}, - # ).json() - - # This assignment shouldn't already exist. If it does, just make sure it - # is all the same. - assignment_stub = AM.get_stub_if_exists(amt_assignment_id=amt_assignment_id) - if assignment_stub: - print(f"{assignment_stub=}") - assert assignment_stub.amt_worker_id == amt_worker_id - assert assignment_stub.amt_assignment_id == amt_assignment_id - assert assignment_stub.created_at > ( - datetime.now(tz=timezone.utc) - timedelta(minutes=90) - ) - return None - - assignment_stub = AssignmentStub( - amt_hit_id=amt_hit_id, - amt_worker_id=amt_worker_id, - amt_assignment_id=amt_assignment_id, - status=AssignmentStatus.Accepted, - hit_id=hit.id, - ) - - AM.create_stub(stub=assignment_stub) - - return None diff --git a/requirements.txt b/requirements.txt index 8976073..adfe6e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -git+ssh://code.g-r-l.com:6611/py-utils@v3.3.4 +git+ssh://code.g-r-l.com:6611/generalresearch@v3.4.7 aiohappyeyeballs==2.6.1 aiohttp==3.13.0 aiosignal==1.4.0 @@ -24,6 +24,7 @@ dnspython==2.8.0 email-validator==2.3.0 Faker==37.11.0 fastapi==0.119.0 +filelock==3.32.5 frozenlist==1.8.0 fsspec==2025.9.0 geoip2==5.1.0 @@ -51,18 +52,21 @@ pandas==2.3.3 pandera==0.26.1 partd==1.4.2 pathspec==0.12.1 +phonenumbers==9.0.38 platformdirs==4.5.0 pluggy==1.6.0 propcache==0.4.1 protobuf==6.32.1 psutil==7.1.1 psycopg==3.2.10 +pyarrow==25.0.1 pycountry==24.6.1 pydantic==2.12.0 pydantic-extra-types==2.10.6 pydantic-settings==2.11.0 pydantic_core==2.41.1 Pygments==2.19.2 +PyJWT==2.13.0 pylibmc==1.6.3 pymemcache==4.0.0 PyMySQL==1.1.2 @@ -74,6 +78,7 @@ pytz==2025.2 PyYAML==6.0.3 redis==6.4.0 requests==2.32.5 +requests-file==3.0.1 requests-mock==1.12.1 s3transfer==0.14.0 scipy==1.16.2 @@ -85,6 +90,7 @@ sniffio==1.3.1 sortedcontainers==2.4.0 starlette==0.48.0 tblib==3.2.0 +tldextract==5.3.2 toolz==1.1.0 tornado==6.5.2 typeguard==4.4.4 diff --git a/tests/__init__.py b/tests/__init__.py index e60faf0..0166072 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,7 +1,7 @@ -import random -import string +from random import choices as rand_choices +from string import ascii_uppercase, digits def generate_amt_id(length: int = 30) -> str: - chars = string.ascii_uppercase + string.digits - return "".join(random.choices(chars, k=length)) + chars = ascii_uppercase + digits + return "".join(rand_choices(chars, k=length)) diff --git a/tests/conftest.py b/tests/conftest.py index 25eb457..7a74fa0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,28 @@ +from __future__ import annotations + import os -from typing import TYPE_CHECKING +import subprocess +import sys +from collections.abc import Callable, Generator +from datetime import UTC, datetime +from pathlib import Path +from random import randint +from typing import TYPE_CHECKING, Any from uuid import uuid4 -from dotenv import load_dotenv + import pytest -from generalresearchutils.pg_helper import PostgresConfig -from tests import generate_amt_id -from _pytest.config import Config -from jb.decorators import CLIENT_CONFIG +import redis +from fastapi.testclient import TestClient +from generalresearch.models.custom_types import InternalHostname, PostgresDict +from generalresearch.pg_helper import PostgresConfig, PostgresDsn +from generalresearch.redis_helper import RedisConfig from mypy_boto3_mturk import MTurkClient +from pydantic import TypeAdapter +from pytest import TempPathFactory + +from jb.decorators import CLIENT_CONFIG, get_redis_config +from jb.main import app +from tests import generate_amt_id if TYPE_CHECKING: from jb.settings import Settings @@ -75,53 +90,281 @@ def pe_id() -> str: @pytest.fixture(scope="session") -def env_file_path(pytestconfig: Config) -> str: - root_path = pytestconfig.rootpath - env_path = os.path.join(root_path, ".env.test") +def settings() -> Settings: + from jb.settings import Settings as JBSettings - if os.path.exists(env_path): - load_dotenv(dotenv_path=env_path, override=True) + return JBSettings() - return env_path + +# --- Database Connectors --- @pytest.fixture(scope="session") -def settings(env_file_path: str) -> "Settings": - from jb.settings import Settings as JBSettings +def postgres_instance(settings: Settings) -> Generator[PostgresDsn]: + """Create a ephemeral postgresql instance for us to use during pytest. - s = JBSettings(_env_file=env_file_path) + This does not create any tables, or schema definitions within the instance. + What this does is simply: - return s + 1. Create a database on a known, consistent, staging or unittest + defined Postgres server. + 2. Return the PostgresDsn of that table -# --- Database Connectors --- + 3. On shutdown, go ahead and delete that database after the + tests have finished. + """ + + msg = "Must define Postgres test settings" + assert settings.testing_postgres, msg + assert settings.testing_postgres_user, msg + assert settings.testing_postgres_pass, msg + + db_uri, db_user, db_pass = ( + settings.testing_postgres, + settings.testing_postgres_user, + settings.testing_postgres_pass, + ) + + # Connect to default DB to create the new one + from psycopg import connect + from psycopg.sql import SQL, Identifier + + now = datetime.now(UTC) + ts: str = now.strftime("%Y-%m-%d") + db_name = f"unittest-{ts}-{uuid4().hex[:6]}" + + db_path_connect = f"postgres://{db_user}:{db_pass}@{db_uri}" + db_path = f"{db_path_connect}/{db_name}" + + # The DATABASE does NOT yet exist on the Postgres SERVER, thus + # we first must connect only to the SERVER (eg: default postgres path used) + conn = connect(f"{db_path_connect}/postgres") + conn.autocommit = True + cur = conn.cursor() + cur.execute(SQL("CREATE DATABASE {}").format(Identifier(db_name))) + cur.close() + conn.close() + + yield PostgresDsn(db_path) + + # Teardown: drop the DB after the session + conn = connect(f"{db_path_connect}/postgres") + conn.autocommit = True + cur = conn.cursor() + cur.execute(SQL("DROP DATABASE {} WITH (FORCE)").format(Identifier(db_name))) + cur.close() + conn.close() @pytest.fixture(scope="session") -def redis(settings: "Settings"): - from generalresearchutils.redis_helper import RedisConfig +def django_settings_file( + postgres_instance_dict: PostgresDict, + tmp_path_factory: TempPathFactory, +) -> Callable[..., tuple[str, Path]]: + + def _inner(extra_installed_apps: list[str] | None = None) -> tuple[str, Path]: + installed_apps = [ + "django.contrib.postgres", + "django.contrib.contenttypes", + ] + (extra_installed_apps or []) + + settings_dir = tmp_path_factory.mktemp("django-settings") + settings_module = "test_settings" + + settings_content = f"""DATABASES = {{ + "default": {{ + "ENGINE": "django.db.backends.postgresql", + "NAME": {postgres_instance_dict["name"]!r}, + "USER": {postgres_instance_dict["username"]!r}, + "PASSWORD": {postgres_instance_dict["password"]!r}, + "HOST": {postgres_instance_dict["host"]!r}, + "PORT": {postgres_instance_dict["port"]!r}, + }} +}} +INSTALLED_APPS = {installed_apps!r} +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_L10N = True +USE_TZ = True +""" + settings_file_path = settings_dir / f"{settings_module}.py" + settings_file_path.write_text(settings_content, encoding="utf-8") + + return settings_module, settings_dir + + return _inner - redis_config = RedisConfig( - dsn=settings.redis, - decode_responses=True, - socket_timeout=settings.redis_timeout, - socket_connect_timeout=settings.redis_timeout, + +@pytest.fixture(scope="session") +def postgres_instance_dict( + postgres_instance: PostgresDsn, +) -> Generator[PostgresDict]: + host = postgres_instance.hosts()[0] + assert host is not None + + msg = "Must have full Postgres details" + assert host["host"], msg + assert host["username"], msg + assert host["password"], msg + + assert postgres_instance.path + + yield PostgresDict( + username=host["username"], + password=host["password"], + host=host["host"], + name=postgres_instance.path.lstrip("/"), + port=5432, ) - return redis_config.create_redis_client() @pytest.fixture(scope="session") -def pg_config(settings: "Settings") -> PostgresConfig: +def postgres_instance_host( + postgres_instance_dict: PostgresDict, +) -> Generator[InternalHostname]: + adapter = TypeAdapter(InternalHostname) + value = adapter.validate_python(postgres_instance_dict["host"]) + yield value + + +@pytest.fixture(scope="session") +def django_db_factory( + postgres_instance: PostgresDsn, + django_settings_file: Callable[..., tuple[str, Path]], + postgres_instance_dict: PostgresDict, + tmp_path_factory: TempPathFactory, +) -> Callable[..., PostgresDsn | None]: + + _ran = {} + + def _inner( + django_project: str = "carer.mtwerk", + ) -> PostgresDsn | None: + + if _ran.get(django_project, False): + print(f"Already ran django_db_factory:{django_project}") + return postgres_instance + _ran[django_project] = True + + _cwd = None + _manage_path = "generalresearch.thl_django.app.manage" + _settings_module, _settings_dir = django_settings_file( + extra_installed_apps=[ + "carer.mtwerk", + ], + ) + + pythonpath = str(_settings_dir) + if existing_pythonpath := os.environ.get("PYTHONPATH"): + pythonpath += os.pathsep + existing_pythonpath + + env = { + **os.environ, + "DJANGO_SETTINGS_MODULE": _settings_module, + "PYTHONPATH": pythonpath, + } + + # we check right after. if we check now, we won't print if bad + res1 = subprocess.run( # noqa: PLW1510 + [ + sys.executable, + "-m", + _manage_path, + "makemigrations", + f"--settings={_settings_module}", + ], + cwd=str(_cwd) if _cwd is not None else None, + env=env, + capture_output=True, + text=True, + ) + + if res1.returncode != 0: + print("STDOUT:", res1.stdout) + print("STDERR:", res1.stderr) + res1.check_returncode() + + res2 = subprocess.run( # noqa: PLW1510 + [ + sys.executable, + "-m", + _manage_path, + "migrate", + f"--settings={_settings_module}", + ], + env=env, + cwd=str(_cwd) if _cwd is not None else None, + capture_output=True, + text=True, + ) + + if res2.returncode != 0: + print("STDOUT:", res2.stdout) + print("STDERR:", res2.stderr) + res2.check_returncode() + + # 3. Return the Dsn so the factory gives a way to connect + return postgres_instance + + return _inner + + +@pytest.fixture(scope="session") +def pg_config(django_db_factory: Callable[..., PostgresDsn]) -> PostgresConfig: + _dsn = django_db_factory() + return PostgresConfig( - dsn=settings.amt_jb_db, + dsn=_dsn, connect_timeout=1, - statement_timeout=1, + statement_timeout=5, ) +# --- Redis --- + + +@pytest.fixture(scope="session") +def redis_config_db() -> str: + # need to update 'databases' in /etc/redis/redis.conf + # or this won't work and you'll have no indication why ... + return str(randint(99, 1_023)) + + +@pytest.fixture(scope="session") +def redis_config(settings: Settings, redis_config_db: str) -> Generator[RedisConfig]: + assert "unittest" in str(settings.testing_redis) or "127.0.0.1" in str( + settings.testing_redis + ) + + uri = f"redis://{settings.testing_redis}/{redis_config_db}" + + res = subprocess.run( + ["redis-cli", "-u", uri, "SET", "jenkins_lock", "1", "NX", "EX", "3600"], + check=True, + text=True, + capture_output=True, + ) + + if res.stdout.strip() != "OK": + raise ValueError("Redis already locked... aborting.") + + yield RedisConfig( + dsn=uri, + decode_responses=True, + socket_timeout=settings.redis_timeout, + socket_connect_timeout=settings.redis_timeout, + ) + + r = redis.from_url(uri) + r.flushdb() + + # --- Connectors --- @pytest.fixture(scope="session") -def amt_client(settings: "Settings") -> MTurkClient: +def amt_client(settings: Settings) -> MTurkClient: import boto3 client = boto3.client( diff --git a/tests/fixtures/amt.py b/tests/fixtures/amt.py index 65df125..d7cc03d 100644 --- a/tests/fixtures/amt.py +++ b/tests/fixtures/amt.py @@ -1,17 +1,18 @@ -import pytest import copy - +from collections.abc import Callable from datetime import datetime, timedelta -from typing import Callable from uuid import uuid4 + +import pytest from dateutil.tz import tzlocal from mypy_boto3_mturk.type_defs import ( - GetHITResponseTypeDef, CreateHITTypeResponseTypeDef, - ResponseMetadataTypeDef, CreateHITWithHITTypeResponseTypeDef, GetAssignmentResponseTypeDef, + GetHITResponseTypeDef, + ResponseMetadataTypeDef, ) + from jb.managers.amt import APPROVAL_MESSAGE, NO_WORK_APPROVAL_MESSAGE from tests import generate_amt_id diff --git a/tests/fixtures/flow.py b/tests/fixtures/flow.py index dd2f83e..7bf8b4f 100644 --- a/tests/fixtures/flow.py +++ b/tests/fixtures/flow.py @@ -1,18 +1,21 @@ -from datetime import timezone, datetime -from typing import Dict, Callable, Any, Optional +from collections.abc import Callable +from datetime import datetime, timezone +from typing import Any 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, +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 ( - GetHITResponseTypeDef, GetAssignmentResponseTypeDef, + GetHITResponseTypeDef, ) from jb.config import settings @@ -20,8 +23,6 @@ from jb.managers.amt import ( APPROVAL_MESSAGE, BONUS_MESSAGE, ) -from generalresearchutils.currency import USDCent -from generalresearchutils.models.thl.definitions import PayoutStatus @pytest.fixture @@ -31,16 +32,16 @@ def approved_assignment_stubs( amt_assignment_id: str, amt_hit_id: str, hit_response_reviewing: GetHITResponseTypeDef, -) -> Callable[..., list[Dict[str, Any]]]: +) -> 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]]: + override_response: str | None = None, + override_approve_response: str | None = None, + ) -> list[dict[str, Any]]: response = override_response or assignment_response approve_response = ( @@ -84,11 +85,11 @@ def approved_assignment_stubs( @pytest.fixture def approved_assignment_stubs_w_bonus( - approved_assignment_stubs: Callable[..., list[Dict[str, Any]]], + approved_assignment_stubs: Callable[..., list[dict[str, Any]]], amt_worker_id: str, amt_assignment_id: str, pe_id: str, -) -> list[Dict[str, Any]]: +) -> list[dict[str, Any]]: now = datetime.now(tz=timezone.utc) stubs = approved_assignment_stubs().copy() @@ -132,16 +133,16 @@ def rejected_assignment_stubs( amt_assignment_id: str, amt_hit_id: str, hit_response_reviewing: GetHITResponseTypeDef, -) -> Callable[..., list[Dict[str, Any]]]: +) -> 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]]: + override_response: str | None = None, + override_reject_response: str | None = None, + ) -> list[dict[str, Any]]: response = override_response or assignment_response reject_response = ( @@ -233,7 +234,7 @@ def mock_thl_responses( elif url == wallet_url: class MockThlWalletResponse: - def json(self) -> Dict[str, Any]: + def json(self) -> dict[str, Any]: return { "wallet": { "amount": wallet_redeemable_amount, @@ -246,7 +247,7 @@ def mock_thl_responses( elif url == status_url: class MockThlStatusResponse: - def json(self) -> Dict[str, Any]: + def json(self) -> dict[str, Any]: return { "tsid": tsid, "product_id": str(settings.product_id), diff --git a/tests/fixtures/http.py b/tests/fixtures/http.py index 5f50580..e38c853 100644 --- a/tests/fixtures/http.py +++ b/tests/fixtures/http.py @@ -1,20 +1,20 @@ +import json +import secrets +from collections.abc import AsyncGenerator +from typing import Any + import httpx -import redis import pytest import requests_mock from asgi_lifespan import LifespanManager -from httpx import AsyncClient, ASGITransport -from typing import Dict, Any, AsyncGenerator +from generalresearch.redis_helper import RedisConfig +from httpx import ASGITransport, AsyncClient +from jb.config import JB_EVENTS_STREAM, settings +from jb.decorators import get_redis_config from jb.main import app -import json - -from httpx import AsyncClient -import secrets - -from jb.models.hit import Hit from jb.models.assignment import AssignmentStub -from jb.config import JB_EVENTS_STREAM, settings +from jb.models.hit import Hit from tests import generate_amt_id @@ -24,7 +24,8 @@ def anyio_backend(): @pytest.fixture(scope="session") -async def httpxclient() -> AsyncGenerator[AsyncClient, None]: +async def httpxclient(redis_config: RedisConfig) -> AsyncGenerator[AsyncClient, None]: + app.dependency_overrides[get_redis_config] = lambda: redis_config # limiter.enabled = True # limiter.reset() app.testing = True @@ -38,6 +39,8 @@ async def httpxclient() -> AsyncGenerator[AsyncClient, None]: yield client await client.aclose() + app.dependency_overrides.clear() + @pytest.fixture() def no_limit(): @@ -69,7 +72,7 @@ def generate_hex_id(length: int = 40) -> str: @pytest.fixture def mturk_event_body_record( hit_record: Hit, assignment_stub_record: AssignmentStub -) -> Dict[str, Any]: +) -> dict[str, Any]: return { "Type": "Notification", "Message": json.dumps( @@ -93,9 +96,11 @@ def mturk_event_body_record( @pytest.fixture() -def clean_mturk_events_redis_stream(redis: redis.Redis): - redis.xtrim(JB_EVENTS_STREAM, maxlen=0) - assert redis.xlen(JB_EVENTS_STREAM) == 0 +def clean_mturk_events_redis_stream(redis_config: RedisConfig): + redis_client = redis_config.create_redis_client() + + redis_client.xtrim(JB_EVENTS_STREAM, maxlen=0) + assert redis_client.xlen(JB_EVENTS_STREAM) == 0 yield - redis.xtrim(JB_EVENTS_STREAM, maxlen=0) - assert redis.xlen(JB_EVENTS_STREAM) == 0 + redis_client.xtrim(JB_EVENTS_STREAM, maxlen=0) + assert redis_client.xlen(JB_EVENTS_STREAM) == 0 diff --git a/tests/fixtures/managers.py b/tests/fixtures/managers.py index a3187d7..8f87e5d 100644 --- a/tests/fixtures/managers.py +++ b/tests/fixtures/managers.py @@ -1,14 +1,15 @@ from typing import TYPE_CHECKING + import pytest -from jb.managers import Permission -from generalresearchutils.pg_helper import PostgresConfig +from generalresearch.managers.base import Permission +from generalresearch.pg_helper import PostgresConfig from mypy_boto3_mturk import MTurkClient if TYPE_CHECKING: - from jb.managers.hit import HitQuestionManager, HitTypeManager, HitManager + from jb.managers.amt import AMTManager from jb.managers.assignment import AssignmentManager from jb.managers.bonus import BonusManager - from jb.managers.amt import AMTManager + from jb.managers.hit import HitManager, HitQuestionManager, HitTypeManager # --- Managers --- diff --git a/tests/fixtures/models.py b/tests/fixtures/models.py index 671c7b3..157daec 100644 --- a/tests/fixtures/models.py +++ b/tests/fixtures/models.py @@ -1,23 +1,22 @@ -from datetime import timezone, datetime +from collections.abc import Callable, Generator +from datetime import datetime, timedelta, timezone +from typing import TYPE_CHECKING import pytest +from generalresearch.currency import USDCent +from generalresearch.pg_helper import PostgresConfig +from psycopg.errors import ForeignKeyViolation -from jb.models.event import MTurkEvent -from generalresearchutils.pg_helper import PostgresConfig - -from datetime import datetime, timezone, timedelta -from typing import Optional, TYPE_CHECKING, Callable, Generator from jb.managers.amt import AMTManager -from jb.models.assignment import AssignmentStub, Assignment -from generalresearchutils.currency import USDCent -from jb.models.definitions import HitStatus, HitReviewStatus, AssignmentStatus -from jb.models.hit import HitType, HitQuestion, Hit +from jb.models.assignment import Assignment, AssignmentStub +from jb.models.definitions import AssignmentStatus, HitReviewStatus, HitStatus +from jb.models.event import MTurkEvent +from jb.models.hit import Hit, HitQuestion, HitType from tests import generate_amt_id -from psycopg.errors import ForeignKeyViolation if TYPE_CHECKING: - from jb.managers.hit import HitQuestionManager, HitTypeManager, HitManager from jb.managers.assignment import AssignmentManager + from jb.managers.hit import HitManager, HitQuestionManager, HitTypeManager # --- MTurk Event --- @@ -76,10 +75,9 @@ def hit_type_record( yield ht try: - with pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute("DELETE FROM mtwerk_hittype WHERE id=%s", (ht.id,)) - conn.commit() + with pg_config.make_connection() as conn, conn.cursor() as c: + c.execute("DELETE FROM mtwerk_hittype WHERE id=%s", (ht.id,)) + conn.commit() except ForeignKeyViolation: pass # DB gets dropped anyway, don't care @@ -99,10 +97,9 @@ def hit_type_record_with_amt_id( yield ht try: - with pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute("DELETE FROM mtwerk_hittype WHERE id=%s", (ht.id,)) - conn.commit() + with pg_config.make_connection() as conn, conn.cursor() as c: + c.execute("DELETE FROM mtwerk_hittype WHERE id=%s", (ht.id,)) + conn.commit() except ForeignKeyViolation: pass # DB gets dropped anyway, don't care @@ -166,10 +163,9 @@ def hit_record( yield hit try: - with pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute("DELETE FROM mtwerk_hit WHERE id=%s", (hit.id,)) - conn.commit() + with pg_config.make_connection() as conn, conn.cursor() as c: + c.execute("DELETE FROM mtwerk_hit WHERE id=%s", (hit.id,)) + conn.commit() except ForeignKeyViolation: pass # DB gets dropped anyway, don't care @@ -228,12 +224,11 @@ def assignment_stub_record( yield assignment_stub try: - with pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute( - "DELETE FROM mtwerk_assignment WHERE id=%s", (assignment_stub.id,) - ) - conn.commit() + with pg_config.make_connection() as conn, conn.cursor() as c: + c.execute( + "DELETE FROM mtwerk_assignment WHERE id=%s", (assignment_stub.id,) + ) + conn.commit() except ForeignKeyViolation: pass # DB gets dropped anyway, don't care @@ -255,19 +250,18 @@ def assignment_record( yield assignment try: - with pg_config.make_connection() as conn: - with conn.cursor() as c: - c.execute("DELETE FROM mtwerk_assignment WHERE id=%s", (assignment.id,)) - conn.commit() + with pg_config.make_connection() as conn, conn.cursor() as c: + c.execute("DELETE FROM mtwerk_assignment WHERE id=%s", (assignment.id,)) + conn.commit() except ForeignKeyViolation: pass # DB gets dropped anyway, don't care @pytest.fixture -def assignment_factory(hit_record: Hit) -> Callable[[Optional[str]], Assignment]: +def assignment_factory(hit_record: Hit) -> Callable[[str | None], Assignment]: - def _inner(amt_worker_id: Optional[str] = None) -> Assignment: + def _inner(amt_worker_id: str | None = None) -> Assignment: now = datetime.now(tz=timezone.utc) amt_assignment_id = generate_amt_id() amt_worker_id = amt_worker_id or generate_amt_id() @@ -292,7 +286,7 @@ def assignment_record_factory( am: "AssignmentManager", assignment_factory: Callable[..., Assignment] ) -> Callable[..., Assignment]: - def _inner(hit_id: int, amt_worker_id: Optional[str] = None) -> Assignment: + def _inner(hit_id: int, amt_worker_id: str | None = None) -> Assignment: a = assignment_factory(amt_worker_id=amt_worker_id) a.hit_id = hit_id am.create_stub(a) diff --git a/tests/flow/test_tasks.py b/tests/flow/test_tasks.py index 3a71504..f28a6aa 100644 --- a/tests/flow/test_tasks.py +++ b/tests/flow/test_tasks.py @@ -1,34 +1,36 @@ import logging +from collections.abc import Callable from contextlib import contextmanager -from typing import Callable, Dict, Any +from typing import Any import pytest from botocore.stub import Stubber +from generalresearch.currency import USDCent +from mypy_boto3_mturk import MTurkClient +from mypy_boto3_mturk.type_defs import ( + GetAssignmentResponseTypeDef, +) + from jb.flow.assignment_tasks import process_assignment_submitted from jb.managers.amt import ( - AMTManager, APPROVAL_MESSAGE, + NO_WORK_APPROVAL_MESSAGE, REJECT_MESSAGE_BADDIE, - REJECT_MESSAGE_UNKNOWN_ASSIGNMENT, REJECT_MESSAGE_NO_WORK, - NO_WORK_APPROVAL_MESSAGE, -) -from mypy_boto3_mturk.type_defs import ( - GetAssignmentResponseTypeDef, + REJECT_MESSAGE_UNKNOWN_ASSIGNMENT, + AMTManager, ) -from generalresearchutils.currency import USDCent from jb.managers.assignment import AssignmentManager from jb.managers.bonus import BonusManager from jb.managers.hit import HitManager +from jb.models.assignment import Assignment, AssignmentStub from jb.models.definitions import AssignmentStatus from jb.models.event import MTurkEvent from jb.models.hit import Hit -from jb.models.assignment import Assignment, AssignmentStub -from mypy_boto3_mturk import MTurkClient @contextmanager -def amt_stub_context(amt_client: MTurkClient, responses: list[Dict[str, Any]]): +def amt_stub_context(amt_client: MTurkClient, responses: list[dict[str, Any]]): # ty chatgpt for this with Stubber(amt_client) as stub: @@ -83,7 +85,7 @@ class TestProcessAssignmentSubmitted: mturk_event: MTurkEvent, amt_assignment_id: str, caplog: pytest.LogCaptureFixture, - rejected_assignment_stubs: Callable[..., list[Dict[str, Any]]], + rejected_assignment_stubs: Callable[..., list[dict[str, Any]]], ): # These records are auto cleaned up, so we need to explicitly create @@ -107,9 +109,9 @@ class TestProcessAssignmentSubmitted: ) stub.assert_no_pending_responses() - assert f"No assignment found in DB: {amt_assignment_id}" in caplog.text - assert f"Rejected assignment doesn't exist in DB. Creating ... " in caplog.text - assert f"Rejected assignment: " in caplog.text + # assert f"No assignment found in DB: {amt_assignment_id}" in caplog.text + assert "Rejected assignment doesn't exist in DB. Creating ... " in caplog.text + assert "Rejected assignment: " in caplog.text stub.assert_no_pending_responses() ass = am.get(amt_assignment_id=amt_assignment_id) @@ -128,7 +130,7 @@ class TestProcessAssignmentSubmitted: assignment_stub_record: AssignmentStub, caplog: pytest.LogCaptureFixture, mock_thl_responses: Callable[..., None], - rejected_assignment_stubs: Callable[..., list[Dict[str, Any]]], + rejected_assignment_stubs: Callable[..., list[dict[str, Any]]], ): # An assignment is submitted. The hit and AssignmentStub exist in the # DB. We think we're going to approve the Assignment, but the @@ -140,7 +142,6 @@ class TestProcessAssignmentSubmitted: _ = assignment_stub_record amt_stubs = rejected_assignment_stubs(reject_reason=REJECT_MESSAGE_BADDIE) - mock_thl_responses(user_blocked=True) with amt_stub_context(amt_client, amt_stubs) as stub, caplog.at_level( @@ -151,9 +152,9 @@ class TestProcessAssignmentSubmitted: ) stub.assert_no_pending_responses() - assert f"No assignment found in DB: {amt_assignment_id}" not in caplog.text - assert f"blocked or not exists" in caplog.text - assert f"Rejected assignment: " in caplog.text + # assert f"No assignment found in DB: {amt_assignment_id}" not in caplog.text + assert "blocked or not exists" in caplog.text + assert "Rejected assignment: " in caplog.text ass = am.get(amt_assignment_id=amt_assignment_id) assert ass.status == AssignmentStatus.Rejected @@ -171,7 +172,7 @@ class TestProcessAssignmentSubmitted: assignment_stub_record: AssignmentStub, caplog: pytest.LogCaptureFixture, mock_thl_responses: Callable[..., None], - approved_assignment_stubs: Callable[..., list[Dict[str, Any]]], + approved_assignment_stubs: Callable[..., list[dict[str, Any]]], assignment_response_approved_no_tsid: GetAssignmentResponseTypeDef, assignment_response_no_tsid: GetAssignmentResponseTypeDef, ): @@ -202,8 +203,8 @@ class TestProcessAssignmentSubmitted: stub.assert_no_pending_responses() assert f"No assignment found in DB: {amt_assignment_id}" not in caplog.text - assert f"Assignment submitted with no tsid" in caplog.text - assert f"Approved assignment: " in caplog.text + assert "Assignment submitted with no tsid" in caplog.text + assert "Approved assignment: " in caplog.text ass = am.get(amt_assignment_id=amt_assignment_id) assert ass.status == AssignmentStatus.Approved @@ -222,7 +223,7 @@ class TestProcessAssignmentSubmitted: assignment_stub_record: AssignmentStub, caplog: pytest.LogCaptureFixture, mock_thl_responses: Callable[..., None], - rejected_assignment_stubs: Callable[..., list[Dict[str, Any]]], + rejected_assignment_stubs: Callable[..., list[dict[str, Any]]], assignment_response_factory_rejected_no_tsid: Callable[ ..., GetAssignmentResponseTypeDef ], @@ -273,8 +274,8 @@ class TestProcessAssignmentSubmitted: stub.assert_no_pending_responses() assert f"No assignment found in DB: {amt_assignment_id}" not in caplog.text - assert f"Assignment submitted with no tsid" in caplog.text - assert f"Rejected assignment: " in caplog.text + assert "Assignment submitted with no tsid" in caplog.text + assert "Rejected assignment: " in caplog.text # It will exist in the db since we can validate the model. ass = am.get(amt_assignment_id=amt_assignment_id) @@ -293,7 +294,7 @@ class TestProcessAssignmentSubmitted: assignment_stub_record: Assignment, caplog: pytest.LogCaptureFixture, mock_thl_responses: Callable[..., None], - approved_assignment_stubs: Callable[..., list[Dict[str, Any]]], + approved_assignment_stubs: Callable[..., list[dict[str, Any]]], ): _ = assignment_stub_record # we need this to make the assignment stub in the db @@ -330,7 +331,7 @@ class TestProcessAssignmentSubmitted: assignment_stub_record: Assignment, caplog: pytest.LogCaptureFixture, mock_thl_responses: Callable[..., None], - approved_assignment_stubs_w_bonus: list[Dict[str, Any]], + approved_assignment_stubs_w_bonus: list[dict[str, Any]], ): _ = assignment_stub_record # we need this to make the assignment stub in the db mock_thl_responses(status_complete=True, wallet_redeemable_amount=10) diff --git a/tests/http/test_auth.py b/tests/http/test_auth.py new file mode 100644 index 0000000..1fa1335 --- /dev/null +++ b/tests/http/test_auth.py @@ -0,0 +1,120 @@ +from urllib.parse import parse_qs, urlparse + +import pytest +from httpx import AsyncClient + +from jb.api.auth import SESSION_COOKIE_NAME +from jb.dependencies import get_gr_api_manager +from jb.main import app +from jb.models.auth import User + + +class FakeGRApiManager: + def __init__(self): + self.users: dict[str, User] = {} + self.amt_worker_ids: set[str] = set() + self.transition_calls: list[tuple[User, str]] = [] + + def ensure_user_exists(self, user: User) -> User: + self.users.setdefault(user.product_user_id, user) + return self.users[user.product_user_id] + + def get_user(self, product_user_id: str) -> User: + return self.users[product_user_id] + + def add_amt_user(self, amt_worker_id: str) -> None: + self.amt_worker_ids.add(amt_worker_id) + + def transition_user_from_amt(self, user: User, amt_worker_id: str) -> User: + self.transition_calls.append((user, amt_worker_id)) + if amt_worker_id not in self.amt_worker_ids: + raise ValueError(f"User {amt_worker_id} does not exist") + + self.amt_worker_ids.remove(amt_worker_id) + self.users[user.product_user_id] = user + return user + + +@pytest.fixture +def email() -> str: + return "unittest@generalresearch.com" + + +@pytest.fixture +def fake_gr_api_manager(): + manager = FakeGRApiManager() + app.dependency_overrides[get_gr_api_manager] = lambda: manager + yield manager + app.dependency_overrides.pop(get_gr_api_manager, None) + + +class TestAuth: + @pytest.mark.anyio + async def test_magic_link( + self, + httpxclient: AsyncClient, + fake_gr_api_manager: FakeGRApiManager, + email: str, + ): + client = httpxclient + + res = await client.post("/auth/magic-link/request", json={"email": email}) + d = res.json() + assert res.status_code == 200 + assert d["magic_link"] + + token = parse_qs(urlparse(d["magic_link"]).query)["token"][0] + + url = "/auth/magic-link/exchange" + body = {"token": token} + res = await client.post(url, json=body) + assert res.status_code == 204 + assert client.cookies.get(SESSION_COOKIE_NAME) + + res = await client.get("/auth/session") + assert res.status_code == 200 + assert res.json()["email"] == email + + @pytest.mark.anyio + async def test_amt_account_link( + self, + httpxclient: AsyncClient, + fake_gr_api_manager: FakeGRApiManager, + email: str, + amt_worker_id: str, + ): + client = httpxclient + fake_gr_api_manager.add_amt_user(amt_worker_id) + + res = await client.post( + "/auth/link-amt/request", + json={"email": email, "amt_worker_id": amt_worker_id}, + ) + d = res.json() + assert res.status_code == 200 + assert d["magic_link"] + assert fake_gr_api_manager.transition_calls == [] + + token = parse_qs(urlparse(d["magic_link"]).query)["token"][0] + + url = "/auth/link-amt/exchange" + body = {"token": token} + res = await client.post(url, json=body) + assert res.status_code == 204 + assert client.cookies.get(SESSION_COOKIE_NAME) + assert len(fake_gr_api_manager.transition_calls) == 1 + transitioned_user, transitioned_amt_worker_id = ( + fake_gr_api_manager.transition_calls[0] + ) + assert transitioned_user.email == email + assert transitioned_amt_worker_id == amt_worker_id + assert amt_worker_id not in fake_gr_api_manager.amt_worker_ids + + res = await client.get("/auth/session") + assert res.status_code == 200 + assert res.json()["email"] == email + + # Make sure we can't do it again + res = await client.post(url, json=body) + assert res.status_code == 401 + assert len(fake_gr_api_manager.transition_calls) == 1 diff --git a/tests/http/test_magic.py b/tests/http/test_magic.py new file mode 100644 index 0000000..f645677 --- /dev/null +++ b/tests/http/test_magic.py @@ -0,0 +1,48 @@ +from uuid import uuid4 + +from generalresearch.redis_helper import RedisConfig + +from jb.api.magic_token import ( + consume_amt_account_link_token, + consume_magic_token, + create_amt_account_link_token, + create_magic_token, +) +from jb.models.auth import AmtAccountLink + + +class TestViewFunctions: + + def test_create_amt_account_link_token( + self, redis_config: RedisConfig, amt_worker_id: str + ): + email = f"{uuid4().hex[:8]}@jamesbillings67.com" + res = create_amt_account_link_token( + email=email, amt_worker_id=amt_worker_id, redis_config=redis_config + ) + assert isinstance(res, str) + + def test_create_and_retrieve_token( + self, redis_config: RedisConfig, amt_worker_id: str + ): + email = f"{uuid4().hex[:8]}@jamesbillings67.com" + token = create_amt_account_link_token( + email=email, amt_worker_id=amt_worker_id, redis_config=redis_config + ) + + res = consume_amt_account_link_token(token=token, redis_config=redis_config) + assert isinstance(res, AmtAccountLink) + assert res.email == email + assert res.amt_worker_id == amt_worker_id + + def test_create_magic_link(self, redis_config: RedisConfig, amt_worker_id: str): + email = f"{uuid4().hex[:8]}@jamesbillings67.com" + res = create_magic_token(user_email=email, redis_config=redis_config) + assert isinstance(res, str) + + def test_consume_magic_link(self, redis_config: RedisConfig, amt_worker_id: str): + email = f"{uuid4().hex[:8]}@jamesbillings67.com" + token = create_magic_token(user_email=email, redis_config=redis_config) + + res = consume_magic_token(token=token, redis_config=redis_config) + assert res == email diff --git a/tests/http/test_notifications.py b/tests/http/test_notifications.py index 508b236..3df2423 100644 --- a/tests/http/test_notifications.py +++ b/tests/http/test_notifications.py @@ -1,14 +1,15 @@ -import pytest import json -import redis -from typing import Dict, Any -from httpx import AsyncClient +from typing import Any from uuid import uuid4 +import pytest +from generalresearch.redis_helper import RedisConfig +from httpx import AsyncClient + from jb.config import JB_EVENTS_STREAM, settings +from jb.models.assignment import AssignmentStub from jb.models.event import MTurkEvent from jb.models.hit import Hit -from jb.models.assignment import AssignmentStub class TestNotifications: @@ -51,13 +52,14 @@ class TestNotifications: @pytest.mark.anyio async def test_mturk_notifications( self, - redis: redis.Redis, + redis_config: RedisConfig, httpxclient: AsyncClient, hit_record: Hit, assignment_stub_record: AssignmentStub, - mturk_event_body_record: Dict[str, Any], + mturk_event_body_record: dict[str, Any], ): client = httpxclient + redis_client = redis_config.create_redis_client() json_msg = json.loads(mturk_event_body_record["Message"]) # Assert the mturk event is owned by the correct account @@ -76,7 +78,7 @@ class TestNotifications: ) # Confirm the stream is empty - assert redis.xlen(JB_EVENTS_STREAM) == 0 + assert redis_client.xlen(JB_EVENTS_STREAM) == 0 res = await client.post( url=f"/{settings.sns_path}/", json=mturk_event_body_record @@ -85,20 +87,20 @@ class TestNotifications: # Now that we POSTed, confirm the stream has 1 event in it # Confirm the stream is empty - assert redis.xlen(JB_EVENTS_STREAM) == 1 + assert redis_client.xlen(JB_EVENTS_STREAM) == 1 # AMT SNS needs to receive a 200 response to stop retrying the notification assert res.status_code == 200 assert res.json() == {"status": "ok"} # Check that the event was enqueued in Redis - msg_res = redis.xread(streams={JB_EVENTS_STREAM: 0}, count=1, block=100) + msg_res = redis_client.xread(streams={JB_EVENTS_STREAM: 0}, count=1, block=100) msg_res = msg_res[0][1][0] msg_id, msg = msg_res - redis.xdel(JB_EVENTS_STREAM, msg_id) + redis_client.xdel(JB_EVENTS_STREAM, msg_id) # After running xdel, we can confirm the stream is empty - assert redis.xlen(JB_EVENTS_STREAM) == 0 + assert redis_client.xlen(JB_EVENTS_STREAM) == 0 msg_json = msg["data"] event = MTurkEvent.model_validate_json(msg_json) diff --git a/tests/http/test_preview.py b/tests/http/test_preview.py index 467c63c..39a6f5b 100644 --- a/tests/http/test_preview.py +++ b/tests/http/test_preview.py @@ -3,8 +3,9 @@ import pytest from httpx import AsyncClient -from jb.models.hit import Hit + from jb.models.assignment import AssignmentStub +from jb.models.hit import Hit class TestPreview: diff --git a/tests/http/test_work.py b/tests/http/test_work.py index 66251f6..9eee15a 100644 --- a/tests/http/test_work.py +++ b/tests/http/test_work.py @@ -1,9 +1,9 @@ import pytest from httpx import AsyncClient -from jb.models.hit import Hit -from jb.models.assignment import AssignmentStub from jb.managers.assignment import AssignmentManager +from jb.models.assignment import AssignmentStub +from jb.models.hit import Hit class TestWork: @@ -16,7 +16,6 @@ class TestWork: amt_assignment_id: str, amt_worker_id: str, ): - client = httpxclient assert isinstance(hit_record.id, int) @@ -25,7 +24,7 @@ class TestWork: "assignmentId": amt_assignment_id, "hitId": hit_record.amt_hit_id, } - res = await client.get("/work/", params=params) + res = await httpxclient.get("/work/", params=params) assert res.status_code == 200 @pytest.mark.anyio @@ -36,8 +35,6 @@ class TestWork: amt_assignment_id: str, amt_worker_id: str, ): - client = httpxclient - # Because no AssignmentStub record is created, and we're just using # random strings as IDs, we should also confirm that the Hit record # is not a saved record. @@ -48,8 +45,13 @@ class TestWork: "assignmentId": amt_assignment_id, "hitId": hit.amt_hit_id, } - res = await client.get("/work/", params=params) - assert res.status_code == 500 + res = await httpxclient.get("/work/", params=params) + + # This either results a 302 redirect to the Preview page, + # or a 200. In previous tests, it expected a 500 but is + # unclear what that behavior was intended for, but does + # not seem to be the expected response anyway. + assert res.status_code == 200 @pytest.mark.anyio async def test_work_assignment_stub_existing( @@ -61,7 +63,6 @@ class TestWork: amt_assignment_id: str, amt_worker_id: str, ): - client = httpxclient # Because the AssignmentStub is created with a reference to the Hit, # the Hit is actually a "Hit Record" (with a primary key), so it's @@ -78,7 +79,7 @@ class TestWork: "assignmentId": assignment_stub_record.amt_assignment_id, "hitId": hit.amt_hit_id, } - res = await client.get("/work/", params=params) + res = await httpxclient.get("/work/", params=params) assert res.status_code == 200 # Confirm that it exists in the database @@ -96,7 +97,6 @@ class TestWork: amt_assignment_id: str, amt_worker_id: str, ): - client = httpxclient # Confirm that it exists in the database before the call res = am.get_stub_if_exists(amt_assignment_id=amt_assignment_id) @@ -107,10 +107,16 @@ class TestWork: "assignmentId": assignment_stub.amt_assignment_id, "hitId": hit_record.amt_hit_id, } - res = await client.get("/work/", params=params) + res = await httpxclient.get("/work/", params=params) assert res.status_code == 200 # Confirm that it exists in the database res = am.get_stub_if_exists(amt_assignment_id=amt_assignment_id) - assert isinstance(res, AssignmentStub) - assert isinstance(res.id, int) + # assert isinstance(res, AssignmentStub) + # assert isinstance(res.id, int) + + # As of Sep 10th, 2026 - I don't see any logic where the /work/ + # would go ahead and create the Assignment Stub. Maybe it was moved + # somewhere else, but it would continue to be None as the /work/ + # page only returns back the template or a redirect.. - Max + assert res is None diff --git a/tests/managers/test_amt.py b/tests/managers/test_amt.py index a20d0d4..6a944a8 100644 --- a/tests/managers/test_amt.py +++ b/tests/managers/test_amt.py @@ -1,14 +1,13 @@ -from jb.managers.amt import AMTManager -from jb.models.hit import HitType, HitQuestion - -from jb.managers.hit import HitQuestionManager, HitTypeManager, HitManager from mypy_boto3_mturk import MTurkClient from mypy_boto3_mturk.type_defs import ( - GetAssignmentResponseTypeDef, GetAccountBalanceResponseTypeDef, ListHITsResponseTypeDef, ) +from jb.managers.amt import AMTManager +from jb.managers.hit import HitManager, HitTypeManager +from jb.models.hit import HitQuestion, HitType + # from jb.decorators import HM # from jb.flow.tasks import refill_hits, check_stale_hits, check_expired_hits diff --git a/tests/managers/test_hit.py b/tests/managers/test_hit.py index 974bd18..7227e4d 100644 --- a/tests/managers/test_hit.py +++ b/tests/managers/test_hit.py @@ -1,5 +1,5 @@ -from jb.models.hit import HitQuestion, HitType, Hit -from jb.managers.hit import HitTypeManager, HitManager +from jb.managers.hit import HitManager, HitTypeManager +from jb.models.hit import Hit, HitQuestion, HitType class TestHitQuestionManager: diff --git a/tests/models/test_assignment.py b/tests/models/test_assignment.py index 2a87364..ecaafe9 100644 --- a/tests/models/test_assignment.py +++ b/tests/models/test_assignment.py @@ -1,8 +1,9 @@ -from jb.models.assignment import Assignment, AssignmentStub from mypy_boto3_mturk.type_defs import ( GetAssignmentResponseTypeDef, ) +from jb.models.assignment import Assignment, AssignmentStub + class TestAssignmentStub: diff --git a/tests/models/test_event.py b/tests/models/test_event.py index 0496574..a4c591d 100644 --- a/tests/models/test_event.py +++ b/tests/models/test_event.py @@ -1,6 +1,5 @@ import pytest - from jb.models.event import MTurkEvent diff --git a/tests/models/test_hit.py b/tests/models/test_hit.py index 3952068..aa48f00 100644 --- a/tests/models/test_hit.py +++ b/tests/models/test_hit.py @@ -1,4 +1,5 @@ import pytest + from jb.models.hit import Hit diff --git a/tests/test_postgres.py b/tests/test_postgres.py new file mode 100644 index 0000000..244c132 --- /dev/null +++ b/tests/test_postgres.py @@ -0,0 +1,69 @@ +import socket +import subprocess +from collections.abc import Callable +from typing import TYPE_CHECKING + +from generalresearch.pg_helper import PostgresConfig +from pydantic import PostgresDsn + +if TYPE_CHECKING: + from generalresearch.models.custom_types import InternalHostname, PostgresDict + + +def is_port_open(host: InternalHostname, port: int = 5432, timeout: int = 3): + try: + with socket.create_connection((host, port), timeout=timeout): + return True + except (TimeoutError, ConnectionRefusedError, OSError): + return False + + +def can_ping(host: InternalHostname): + return ( + subprocess.call( + ["ping", "-c", "1", str(host)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + == 0 + ) + + +class TestPostgresDSN: + + def test_ping(self, postgres_instance_host: InternalHostname): + assert can_ping(host=postgres_instance_host) + + def test_port(self, postgres_instance_host: InternalHostname): + assert is_port_open(host=postgres_instance_host) + + def test_conn(self, postgres_instance: PostgresDsn): + config = PostgresConfig( + dsn=postgres_instance, + connect_timeout=1, + statement_timeout=1, + ) + res = config.execute_sql_query(query="SELECT 1;") + assert len(res) == 1 + + +class TestPostgresDjangoCreation: + + def test_ping(self, postgres_instance_dict: PostgresDict): + assert can_ping(host=postgres_instance_dict["host"]) + + def test_django_creation( + self, + django_db_factory: Callable[..., None], + ): + dsn = django_db_factory() + assert isinstance(dsn, PostgresDsn) + + def test_django_tables(self, pg_config: PostgresConfig): + res = pg_config.execute_sql_query(query=""" + SELECT COUNT(*) + FROM information_schema.tables + WHERE table_schema = 'public'; + """) + assert len(res) == 1 + assert res[0]["count"] == 7 diff --git a/tests_sandbox/__init__.py b/tests_sandbox/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/tests_sandbox/__init__.py +++ /dev/null |
