Pular para o conteúdo principal

@cactus-agents/user

Serviço de gerenciamento de usuário: perfil, segurança, login history, responsible gaming e income report. Framework-agnostic.

Instalação

pnpm add @cactus-agents/user

Uso básico

A forma recomendada é usar createUserFromClient, que aceita um ApiClient diretamente:

import { createApiClient } from '@cactus-agents/api-client';
import { createUserFromClient } from '@cactus-agents/user';

const client = createApiClient({ baseUrl, tenant, language });
const user = createUserFromClient(client);

const history = await user.getLoginHistory(1);

Uso de baixo nível (fetcher manual)

Se precisar de controle total sobre o adapter HTTP, use createUserService. O fetcher precisa implementar get, post, patch e delete:

import { createUserService } from '@cactus-agents/user';

const userService = createUserService({
get: (path) => myHttpClient.get(path),
post: (path, body) => myHttpClient.post(path, body),
patch: (path, body) => myHttpClient.patch(path, body),
delete: (path) => myHttpClient.delete(path),
});

API — UserService

Perfil

updateProfile(id, data)

POST /users/update/{id}

interface UpdateUserPayload {
captcha_token?: string;
kyc_id?: number;
password_check?: string;
state?: string;
zipcode?: string;
city?: string;
address?: string;
phone?: string;
ddi?: string;
affiliation_code?: string;
email?: string;
birth_date?: string;
terms?: boolean;
language?: string;
timezone?: string;
country?: string;
gender?: number;
first_name?: string;
last_name?: string;
mother_name?: string;
}

getAddress()

GET /bff/users/address-by-user — retorna UserAddress.

updateEmail(data)

PATCH /bff/users/self-email

interface UpdateEmailPayload {
email: string;
password_check?: string;
kyc_id?: number;
}

addPhone(data)

PATCH /bff/users/add-phone

interface AddPhonePayload {
phone: string;
ddi: string;
password_check?: string;
kyc_id?: number;
}

updateAddress(data)

PATCH /bff/users/update-address

interface UpdateAddressPayload {
address: string; // Rua, numero e complemento (a API espera "address", nao "street")
city: string;
state: string;
zipcode: string;
country?: string;
ddi?: string;
captcha_token?: string;
password_check?: string;
kyc_id?: number;
}

storeDocument(data)

POST /documents/{endpoint} — Cadastra documento do usuario. O endpoint varia por pais (definido em DocumentConfig.storeEndpoint).

interface StoreDocumentPayload {
number: string; // Numero do documento (raw, sem formatacao)
captcha_token: string; // Token anti-robot
endpoint: "store-user-document" | "store-mex-document"; // Endpoint da API
}
  • "store-user-document" → usado para CPF (BRA)
  • "store-mex-document" → usado para RUT (CHL), CURP (MEX), DNI (PER), Generic (FIN, XYZ)

Exemplo de uso no template (via proxy server-side):

// No front-web-base, a chamada passa por /api/user/store-document
// que le o JWT do cookie HttpOnly e faz a request autenticada
const res = await fetch("/api/user/store-document", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
number: docConfig.strip(documentValue),
captcha_token: "",
endpoint: docConfig.storeEndpoint,
}),
});

updateContracts(type)

PATCH /bff/users/self-contracts — envia { type }.

type ContractType = 'tc' | 'privacy' | 'lgpd' | 'law' | 'migrate' | 'mkt';

updateMarketing(data)

PATCH /bff/users/self-mkt

interface UpdateMarketingPayload {
mkt_accepted: boolean;
}

Segurança

changePassword(id, data)

POST /users/change-password/{id}

interface ChangePasswordPayload {
old_password: string;
password: string;
password_confirmation: string;
captcha_token?: string;
kyc_id?: number;
}

checkPassword(data)

POST /bff/users/check-password — retorna { success: boolean }.

toggleTwoFactor(data)

PATCH /bff/users/self-two-factor

interface TwoFactorPayload {
two_factor_type: 'email' | 'sms';
two_factor_enabled: boolean;
password_check?: string;
kyc_id?: number;
}

getSocialAccounts()

GET /bff/users/account-social — retorna SocialAccount[].

connectSocial(type)

POST /bff/users/account-social/connect/{type}

disconnectSocial(id)

DELETE /bff/users/account-social/{id}

getLoginHistory(page?)

GET /bff/users/login-history?page={page} — default page 1.

Retorna dados já normalizados via transformLoginHistoryResponse. O template não precisa lidar com o shape raw da API.

// Tipo público (o que o template consome)
interface LoginHistoryItem {
userId: number;
ip: string | null;
location: string | null; // "São Paulo, SP" — city + state compostos automaticamente
createdAt: string; // ISO 8601: "2026-03-17T18:14:42"
latitude: string | null;
longitude: string | null;
}

interface LoginHistoryResponse {
data: LoginHistoryItem[];
currentPage: number;
lastPage: number;
perPage: number;
total: number;
}

:::note Shape raw da API A API retorna date (não created_at), campos city/state/latitude/longitude separados, sem id e sem user_agent. O SDK normaliza tudo automaticamente — o template nunca vê o shape raw. :::

Responsible Gaming

updateLimits(data)

PATCH /bff/users/update-limits — atualiza limites de depósito, aposta, perda e/ou tempo.

interface UpdateLimitsPayload {
user_limit_deposit?: number;
user_limit_deposit_active?: number;
user_limit_deposit_period?: string; // 'daily' | 'weekly' | 'monthly'
user_limit_bet?: number;
user_limit_bet_active?: number;
user_limit_bet_period?: string;
user_limit_loss?: number;
user_limit_loss_active?: number;
user_limit_loss_period?: string;
user_limit_time?: number;
user_limit_time_active?: number;
user_limit_time_period?: string;
kyc_id?: number;
password_check?: string;
}

setTimeoutLimits(data)

PATCH /bff/users/timeout-limits — pausa temporária (1 a 45 dias).

interface TimeoutLimitsPayload {
user_pause: number; // dias (1-45)
reason: number;
kyc_id?: number;
password_check?: string;
}

setSelfExclusion(data)

PATCH /bff/users/self-exclusion — autoexclusão por meses ou permanente.

interface SelfExclusionPayload {
user_exclusion: number; // meses (3, 6, 12, 24, 36, 60) ou -1 permanente
reason: number;
kyc_id?: number;
password_check?: string;
}

Income Report (IRPF)

generateIncomeReport(data)

POST /income-report/generate — retorna IncomeReportData.

interface IncomeReportGeneratePayload {
reference_year: number;
type: 'email' | 'download';
}

getIncomeReportStatus(id)

GET /income-report/{id} — retorna IncomeReportData. Usado para polling até o status mudar.

interface IncomeReportData {
id: number;
reference_year: number;
status: 1 | 2 | 3 | 4; // 1=pendente, 2=processando, 3=concluído, 4=falhou
delivery_type: 'email' | 'download';
requested_at: string;
generated_at: string | null;
path: string | null;
download_url: string | null;
}

getAvailableYears()

GET /income-report/available-years — retorna { years: number[] }.

Wallet

:::info Compatibilidade legada O módulo de wallet foi extraído para @cactus-agents/wallet. No @cactus-agents/user permanecem apenas re-exports e compatibilidade temporária. :::

Para novos fluxos, use:

  • createWalletFromClient
  • fetchWalletData
  • aggregateWalletData

Documentacao completa em @cactus-agents/wallet.

Responsible Gaming — Helpers e Constantes

Exportados de @cactus-agents/user (fonte de verdade para regras de responsible gaming). O template nunca redefine essas constantes.

Constantes

// Opções de pausa temporária (dias)
const TIMEOUT_DAYS_OPTIONS = [1, 3, 7, 14, 30, 45] as const;

// Opções de autoexclusão (meses). -1 = permanente.
const SELF_EXCLUSION_MONTHS_OPTIONS = [3, 6, 12, 24, 36, 60, -1] as const;

// Códigos de período da API
const LIMIT_PERIOD = { DAILY: 1, WEEKLY: 2, MONTHLY: 3, YEARLY: 4 };

Helpers

parseLimitPeriod(value)

Converte qualquer representação de período (string da API, número, undefined) para o código numérico correto. Fallback: DAILY (1).

parseLimitPeriod("daily") // → 1
parseLimitPeriod("weekly") // → 2
parseLimitPeriod(3) // → 3
parseLimitPeriod(undefined) // → 1 (daily)

isoDurationToHours(raw)

Parseia ISO 8601 duration ou valor numérico para horas. Retorna null quando inválido.

isoDurationToHours("PT4H") // → 4
isoDurationToHours(4) // → 4
isoDurationToHours(null) // → null
isoDurationToHours("bad") // → null

hoursToIsoDuration(hours)

Converte horas para ISO 8601 duration string.

hoursToIsoDuration(4) // → "PT4H"
hoursToIsoDuration(0.5) // → "PT0.5H"

isPermanentExclusion(months)

Retorna true quando o valor representa exclusão permanente (-1 ou >= 99).

isPermanentExclusion(-1) // → true
isPermanentExclusion(99) // → true
isPermanentExclusion(12) // → false

isLimitChangeBlocked(changeAt)

Retorna true quando o limite foi alterado nas ultimas 24 horas (cooldown ativo). Usado para impedir alteracoes consecutivas antes de 24h.

isLimitChangeBlocked("2024-01-15T10:00:00Z") // → true (se < 24h atras)
isLimitChangeBlocked(null) // → false (sem alteracao anterior)

getLimitChangeUnblocksAt(changeAt)

Retorna a data ISO 8601 de quando o cooldown de 24h vai expirar. Retorna null quando nao ha cooldown ativo.

getLimitChangeUnblocksAt("2024-01-15T10:00:00Z") // → "2024-01-16T10:00:00.000Z"
getLimitChangeUnblocksAt(null) // → null

Restriction Extension Helpers

Quando o usuario ja tem uma autoexclusao ou pausa ativa, ele pode estender (aumentar o periodo) — mas nunca reduzir. Essas funcoes filtram as opcoes disponiveis com base no estado atual do usuario.

RestrictionMinInput

Interface de entrada (subset de AuthUserInfo):

interface RestrictionMinInput {
user_exclusion?: number;
user_exclusion_min?: number | null;
user_pause?: number;
user_pause_min?: number | null;
}
  • user_exclusion_min / user_pause_min — vem da API, define o minimo absoluto.
  • Quando *_min nao existe, o fallback e o valor atual (user_exclusion / user_pause).

getMinSelfExclusionMonths(input)

Retorna o minimo de meses que o usuario pode selecionar. Retorna 0 se nenhuma exclusao esta ativa.

getMinSelfExclusionMonths({ user_exclusion: 3, user_exclusion_min: 6 }) // → 6
getMinSelfExclusionMonths({ user_exclusion: 12 }) // → 12 (min nao definido, usa atual)
getMinSelfExclusionMonths(null) // → 0

filterSelfExclusionOptions(input)

Filtra SELF_EXCLUSION_MONTHS_OPTIONS para apenas periodos >= minimo. Permanente (-1) sempre incluso, exceto quando ja permanente.

filterSelfExclusionOptions({ user_exclusion: 3, user_exclusion_min: 6 })
// → [6, 12, 24, 36, 60, -1] (3 excluido porque min e 6)

filterSelfExclusionOptions({ user_exclusion: 99 })
// → [] (ja permanente — sem opcoes)

filterSelfExclusionOptions(null)
// → [3, 6, 12, 24, 36, 60, -1] (todas as opcoes)

getMinTimeoutDays(input)

Retorna o minimo de dias que o usuario pode selecionar. Retorna 0 se nenhuma pausa esta ativa.

getMinTimeoutDays({ user_pause: 7, user_pause_min: 14 }) // → 14
getMinTimeoutDays({ user_pause: 7 }) // → 7 (min nao definido, usa atual)
getMinTimeoutDays(null) // → 0

filterTimeoutOptions(input)

Filtra TIMEOUT_DAYS_OPTIONS para apenas periodos >= minimo.

filterTimeoutOptions({ user_pause: 7, user_pause_min: 7 })
// → [7, 14, 30, 45] (1 e 3 excluidos)

filterTimeoutOptions({ user_pause: 45, user_pause_min: 60 })
// → [] (nenhuma opcao disponivel)

Uso no template:

import {
TIMEOUT_DAYS_OPTIONS,
SELF_EXCLUSION_MONTHS_OPTIONS,
isoDurationToHours,
parseLimitPeriod,
isPermanentExclusion,
filterSelfExclusionOptions,
filterTimeoutOptions,
} from "@cactus-agents/user";

// No componente — zero lógica local
const currentHours = isoDurationToHours(userInfo.user_limit_time);
const periodCode = parseLimitPeriod(userInfo.user_limit_deposit_period);
const isPermanent = isPermanentExclusion(userInfo.user_exclusion);

// Extensao — opcoes filtradas com base no estado do usuario
const exclusionOptions = filterSelfExclusionOptions(userInfo);
const timeoutOptions = filterTimeoutOptions(userInfo);

Account Restriction

Modulo de deteccao de restricoes de conta. Centraliza toda a logica para verificar se a conta esta em modo restrito (so saques) e qual tipo de restricao esta ativa.

isAccountRestricted(input)

Retorna true quando a conta tem qualquer restricao ativa.

isAccountRestricted(userInfo) // → true quando qualquer restricao esta ativa
isAccountRestricted(null) // → false

getAccountRestriction(input)

Retorna a restricao ativa de maior prioridade com todos os metadados, ou null.

Prioridade (mesma do legado): SPA > Operator > Self-exclusion > Timeout

const r = getAccountRestriction(userInfo);
if (r) {
r.type // "spa_exclusion" | "operator_exclusion" | "self_exclusion" | "timeout"
r.isPermanent // true quando permanente
r.endsAt // ISO date de expiracao (ou null)
r.activatedAt // ISO date de ativacao
r.note // Nota do operador/SPA
r.spaCode // Codigo SPA (99, 102, 103)
r.months // Meses (self-exclusion)
r.days // Dias (timeout)
}

getRestrictionType(input)

Shorthand — retorna apenas o tipo como string ou null.

getRestrictionType(userInfo) // → "self_exclusion" | "timeout" | ... | null

SPA_EXCLUSION_CODES

Codigos de exclusao impostos pela autoridade reguladora (SPA):

const SPA_EXCLUSION_CODES = {
PERMANENT: 99, // Exclusao permanente pelo regulador
SOCIAL_BENEFIT: 102, // Bloqueio por ser beneficiario social (ex: IN 22/24 no Brasil)
CENTRALIZED: 103, // Autoexclusao centralizada via plataforma governamental
};

AccountRestriction

interface AccountRestriction {
type: "self_exclusion" | "timeout" | "operator_exclusion" | "spa_exclusion";
isPermanent: boolean;
endsAt: string | null;
activatedAt: string | null;
note: string | null;
spaCode: number | null;
months: number | null;
days: number | null;
}

AccountRestrictionInput

Subset de AuthUserInfo necessario para as funcoes de deteccao:

interface AccountRestrictionInput {
user_exclusion?: number;
user_exclusion_at?: string | null;
user_exclusion_in?: string | null;
user_pause?: number;
user_pause_at?: string | null;
user_pause_in?: string | null;
operator_exclusion?: number;
operator_exclusion_at?: string | null;
operator_exclusion_note?: string | null;
spa_exclusion?: number;
spa_exclusion_at?: string | null;
spa_exclusion_note?: string | null;
}

Transform

transformLoginHistoryResponse(raw)

Normaliza a resposta raw da API (snake_case, formato de data sem T) para o tipo público camelCase com ISO 8601.

transformLoginHistoryItem(raw)

Normaliza um único item: compõe location a partir de city + state, converte date para createdAt ISO 8601.

transformWalletResponse(raw)

transformWalletResponse permanece exportado por compatibilidade, mas o uso recomendado é importar de @cactus-agents/wallet.

Tipos principais

UserLimits

Retornado dentro do userInfo pelo getUserProfile do auth. Contém todos os limites e exclusões do usuário:

interface UserLimits {
user_pause: number;
user_pause_at: string | null;
user_pause_in: string | null;
user_pause_min: number | null;
user_exclusion: number;
user_exclusion_at: string | null;
user_exclusion_in: string | null;
user_exclusion_min: number | null;
user_limit_deposit: number;
user_limit_deposit_active: number;
user_limit_deposit_period: string;
user_limit_deposit_change: string | null;
user_limit_bet: number;
user_limit_bet_active: number;
user_limit_bet_period: string;
user_limit_bet_change: string | null;
user_limit_loss: number;
user_limit_loss_active: number;
user_limit_loss_period: string;
user_limit_loss_change: string | null;
user_limit_time: number;
user_limit_time_active: number;
user_limit_time_period: string;
user_limit_time_change: string | null;
operator_exclusion: number;
operator_exclusion_at: string | null;
operator_exclusion_note: string;
spa_exclusion: number;
spa_exclusion_at: string | null;
spa_exclusion_note: string;
}

Wallet

interface Wallet {
real: RealWallet;
bonus: BonusWallet;
cashback: CashbackWallet;
}

interface RealWallet {
credit: number;
walletId: number;
withdrawEnabled: boolean;
minWithdrawAmount: number;
maxWithdrawAmount: number;
hasWalletDeposit: boolean;
withdrawEnableNow: boolean;
withdrawNextDate: string;
}

interface BonusWallet {
credit: number;
creditHold: number;
sportsAmountHold: number;
casinoAmountHold: number;
expiryDatetime: string;
}

interface CashbackWallet {
credit: number;
expiresAt: string;
}

UserFetcher

Interface que o createUserService espera receber:

interface UserFetcher {
get<T>(path: string): Promise<T>;
post<T>(path: string, body?: unknown): Promise<T>;
patch<T>(path: string, body?: unknown): Promise<T>;
delete<T>(path: string): Promise<T>;
}

Diferente do ClientLike (get/post), o UserFetcher exige também patch e delete. O wrapper createUserFromClient faz a conversão automaticamente.