@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étodo | Verbo | Endpoint |
|---|---|---|
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étodo | Verbo | Endpoint |
|---|---|---|
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étodo | Verbo | Endpoint |
|---|---|---|
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/packages— confirmado por probe ao vivo (2026-07-03, 7wins-us).API_BASE_URLtermina em/v2, então resolve para/v2/store/packages. É a única variante que o BFF reconhece: responde 403 "Store not available" quando a flagsweepstakes_store_enableddo tenant está desligada, e 404 duro em qualquer alternativa (/api/store/...,/sweepstakes/store/...,/sweepstakes/packages)./store/purchase— nã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 desrc/service.tspede 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`
}
Já 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
| Transform | Entrada → saída |
|---|---|
transformBalances | SweepstakesBalancesRaw → SweepstakesBalances |
transformWalletPreference | WalletPreferenceRaw → WalletPreference |
transformCurrencySwitch | CurrencySwitchRaw → CurrencySwitch |
transformEligibility | RedemptionEligibilityRaw → RedemptionEligibility |
transformRedemption | RedemptionRaw → Redemption |
transformStorePackage | StorePackageRaw → StorePackage |
transformStorePurchase | StorePurchaseRaw → StorePurchase |
buildRedemptionPayload | RedemptionRequestPayload → 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
| Arquivo | Conteúdo |
|---|---|
packages/sweepstakes/src/service.ts | Paths, createSweepstakesFromClient, createSweepstakesService |
packages/sweepstakes/src/transform.ts | Transforms + escala de unidades + toCoinId |
packages/sweepstakes/src/types.ts | Raw e domain types, SweepstakesService |
packages/sweepstakes/src/index.ts | Barrel de exports |