Pular para o conteúdo principal

@cactus-agents/sweepstakes

@cactus-agents/sweepstakes@0.6.0 — orquestrador da modalidade dual-currency GC/SC (sweepstakes): saldos, preferência de carteira, troca de moeda, elegibilidade de resgate e a loja "Buy Gold Coins".

Segue o padrão de fetcher injection dos outros pacotes HTTP e é framework-agnostic. Os services devolvem tipos raw; os transforms são aplicados no call-site (mesmo padrão do payments).

:::warning Não adotado no trunk atual (main-7k) Esta página documenta o contrato do pacote no core, não uma integração ativa. No front-web-base do trunk atual o pacote não é dependência: está ausente do package.json e tem zero imports em app/. A entrada em tsconfig.json é apenas o alias source-direct gerado automaticamente pelo workspace — não é evidência de adoção.

Nenhuma rota SSR, store client-side ou componente de sweepstakes existe aqui; a fiação de consumo vive em outro trunk. Não escreva código no base assumindo que ela existe, e não trate esta página como descrição do que está no ar. :::

Instalação

pnpm add @cactus-agents/sweepstakes

Uso básico

import { createApiClient } from "@cactus-agents/api-client";
import { createSweepstakesFromClient, transformBalances } from "@cactus-agents/sweepstakes";

const client = createApiClient({ baseUrl, tenant, language });
const sweepstakes = createSweepstakesFromClient(client);

const raw = await sweepstakes.getBalances();
const balances = transformBalances(raw);
// balances.gc, balances.sc, balances.scRedeemable, balances.scPlaythroughMet

Fetcher manual (get + post):

import { createSweepstakesService } from "@cactus-agents/sweepstakes";

const sweepstakes = createSweepstakesService({
get: (path) => myHttpClient.get(path),
post: (path, body) => myHttpClient.post(path, body),
});

As duas moedas

type CoinId = "GC" | "SC";
  • GC — Gold Coins: play-for-fun, compráveis na loja, não resgatáveis.
  • SC — Sweeps Coins: resgatáveis por prêmio em dinheiro, sujeitas a play-through.

toCoinId() (interno aos transforms) normaliza qualquer string para essa união e faz fallback em "GC" quando o valor está ausente ou é desconhecido — o default seguro, que nunca concede valor resgatável por erro de parse.

:::note Relação com o CoinId do accounts @cactus-agents/accounts tem seu próprio CoinId, definido como string (opaco), porque a camada de coins de lá é genérica. Aqui a união é fechada em "GC" | "SC". Os dois convivem: o Base busca os saldos com este pacote e os injeta na store de accounts via useSetCoinBalances(); a partir daí useCoinBalance / useActiveCoin / useFormatMoney passam a renderizar coins. Ver accounts/react. :::

Endpoints do SweepstakesService

Carteira e saldos

MétodoVerboEndpoint
getBalances()GET/sweepstakes/balances
getWalletPreference()GET/sweepstakes/wallet-preference
setWalletPreference(coinId)POST/sweepstakes/wallet-preference
switchCurrency(coinId)POST/sweepstakes/currency-switch

setWalletPreference e switchCurrency enviam { coin_type: coinId } — o BFF (Laravel) valida o campo coin_type. switchCurrency encerra a sessão de jogo atual e reabre uma vinculada à nova moeda; a resposta pode trazer session_id.

Resgate

MétodoVerboEndpoint
getEligibility()GET/sweepstakes/eligibility
listRedemptions({ page })GET/sweepstakes/redemptions
getRedemption(id)GET/sweepstakes/redemptions/{id}
requestRedemption(payload)POST/sweepstakes/redemptions

listRedemptions devolve um paginator Laravel ({ data, current_page, last_page, per_page, total }); ?page=N só é anexado quando o caller pede uma página específica.

getRedemption e requestRedemption desembrulham o envelope { data: {...} } do BFF antes de devolver, com fallback para o body plano.

Loja (Buy Gold Coins)

MétodoVerboEndpoint
getStorePackages()GET/store/packages
purchaseStorePackage(packageId)POST/store/purchase
listCoinPackages()stub: Promise.resolve([])

purchaseStorePackage envia { package_id }, cria um Charge (purchase_type=gc_purchase) e devolve a URL de checkout do PSP para redirecionar o browser. Na confirmação do PSP o backend credita GC + SC promocional (idempotente).

:::caution Estado de confirmação dos paths da loja Os comentários no source registram o que foi e o que não foi verificado contra o backend:

  • /store/packagesconfirmado por probe ao vivo (2026-07-03, 7wins-us). API_BASE_URL termina em /v2, então resolve para /v2/store/packages. É a única variante que o BFF reconhece: responde 403 "Store not available" quando a flag sweepstakes_store_enabled do tenant está desligada, e 404 duro em qualquer alternativa (/api/store/..., /sweepstakes/store/..., /sweepstakes/packages).
  • /store/purchasenão foi testado ao vivo (criaria um Charge real). Path inferido como irmão do de packages, body { package_id } conforme o worklog. Confirmar com o backend antes de produção.
  • Os paths /sweepstakes/* assumem montagem na raiz do host, também conforme spec — o comentário no topo de src/service.ts pede confirmação do prefixo.

listCoinPackages() é um stub que sempre retorna []; foi superado por getStorePackages(). :::

Escala de unidades — a armadilha do pacote

:::danger Coin amount cruza o wire em unidades MENORES Valores de GC e SC atravessam o BFF multiplicados por 100 (duas casas implícitas), mesma convenção de price_cents. O app trabalha em unidades inteiras/maiores, então a escala acontece na fronteira do transform: leituras dividem por 100, e o payload de resgate multiplica de volta.

Campos que não são coin (price_cents, ids, flags) passam intactos. :::

O endpoint de balances é o caso especial: ele envia as duas representações de cada moeda — o valor de exibição (gc_balance, sc_balance, já em unidades maiores) e o transacional (gc_balance_cents, sc_balance_cents). transformBalances usa os campos de exibição direto para gc/sc (não re-escala) e guarda os cents para matemática exata:

interface SweepstakesBalances {
gc: number; // exibição
sc: number;
gcCents: number; // transacional
scCents: number;
scRedeemable: number; // SC já liberada para resgate
scRedeemableCents: number;
scLocked: number; // SC ainda travada pelo play-through
scLockedCents: number;
scWagered: number; // progresso do play-through
scPlaythroughRequired: number;
scPlaythroughMultiplier: number;
scPlaythroughMet: boolean;
activeCoinId?: CoinId; // vem de `selected_coin_type`
}

gc_amount e promotional_sc_amount da loja vêm só em cents — por isso transformStorePackage escala os dois, mas deixa price_cents como está (é dinheiro, não coin).

E o payload de resgate:

buildRedemptionPayload({ amountSc: 25.5 });
// → { amount_cents: 2550 }

Elegibilidade de resgate

O BFF expõe flags granulares, não um único booleano. transformEligibility compõe o gate:

eligible = sc_redemption_enabled && kyc_approved && !blocked_reason
interface RedemptionEligibility {
eligible: boolean;
kycRequired?: boolean; // true quando o KYC ainda não foi aprovado (gate mais comum)
scRedemptionEnabled?: boolean;
reasons?: string[]; // reasons[] do BFF + blocked_reason concatenados
minRedeemSc?: number;
jurisdiction?: { country?: string | null; state?: string | null } | null;
}

Campos legados (eligible, kyc_required, min_redeem_sc) continuam aceitos como fallback no raw.

Redemption

interface Redemption {
id: string;
amountSc: number; // já em unidades maiores
status: string;
createdAt: string; // `requested_at`, com fallback em `created_at`
coinType?: CoinId;
payoutUrl?: string | null; // null enquanto pendente, string quando o payout está pronto
rejectionReason?: string | null;
paidAt?: string | null;
method?: string;
}

Os campos do endpoint de detalhe preservam null como distinto de "ausente": um poller consegue diferenciar "pendente" (null) de "a entrada da lista nunca carregou o campo" (undefined). O polling de getRedemption(id) para quando payoutUrl vira uma URL.

Um 404 (id inexistente, ou de outro jogador) sobe como erro do ApiClient — a rota de proxy mapeia para envelope de erro e o client interrompe o polling.

Transforms exportados

TransformEntrada → saída
transformBalancesSweepstakesBalancesRawSweepstakesBalances
transformWalletPreferenceWalletPreferenceRawWalletPreference
transformCurrencySwitchCurrencySwitchRawCurrencySwitch
transformEligibilityRedemptionEligibilityRawRedemptionEligibility
transformRedemptionRedemptionRawRedemption
transformStorePackageStorePackageRawStorePackage
transformStorePurchaseStorePurchaseRawStorePurchase
buildRedemptionPayloadRedemptionRequestPayload → body snake_case

StorePackageListRaw é StorePackageRaw[] | { data?: StorePackageRaw[] } — o backend pode devolver array puro ou paginator, e o call-site normaliza com Array.isArray(raw) ? raw : raw?.data.

Arquivos relevantes

ArquivoConteúdo
packages/sweepstakes/src/service.tsPaths, createSweepstakesFromClient, createSweepstakesService
packages/sweepstakes/src/transform.tsTransforms + escala de unidades + toCoinId
packages/sweepstakes/src/types.tsRaw e domain types, SweepstakesService
packages/sweepstakes/src/index.tsBarrel de exports