Serviço financeiro distribuído para processar BET, WIN, LOSS, REFUND e ROLLBACK com efeitos monetários idempotentes, ledger imutável, inbox/outbox transacionais e concorrência por wallet.
- Bun 1.x e TypeScript strict
- NestJS 11
- TypeORM 0.3
- PostgreSQL 16
- AWS SQS FIFO via LocalStack
- Docker Compose
docker compose up --buildA API fica disponível em http://localhost:3000. O PostgreSQL é publicado em localhost:55432 para evitar colisão com instalações locais na porta padrão. Dentro da rede Compose ele continua em postgres:5432.
Serviços iniciados:
api: API HTTP;wager-consumer: consumidor da fila de entrada;outbox-publisher: publisher dos eventos confirmados;reference-worker: reprocessamento de referências fora de ordem;postgreselocalstack;migrate: job que termina depois de aplicar as migrations.
Para comprovar concorrência entre várias instâncias:
docker compose up -d --scale wager-consumer=3 --scale outbox-publisher=2Nenhum serviço escalável possui container_name.
Pré-requisito: Docker com Compose v2. Em um checkout novo, estes comandos executam as verificações da entrega:
docker compose config --quiet
docker compose up -d --build --scale wager-consumer=3 --scale outbox-publisher=2
docker compose --profile test build integration-tests
docker compose --profile test run --rm --no-deps integration-tests bun run test:all
docker compose --profile test run --rm --no-deps integration-tests bun run typecheck
curl -f http://localhost:3000/health/live
curl -f http://localhost:3000/health/ready
curl -f http://localhost:3000/metricsOs testes usam banco e filas exclusivos, executam todas as migrations up → down → up e podem rodar com os workers principais ativos. O build separado e --no-deps reutilizam a infraestrutura já iniciada. A entrega foi validada com 78 testes passando (41 unitários e 37 de integração), além do TypeScript strict e dos endpoints de operação da API e dos workers.
O workflow Backend checks executa build, TypeScript e a mesma suíte com PostgreSQL/LocalStack reais em pushes e pull requests no GitHub.
bun install --frozen-lockfile
bun run typecheck
bun run testbun run test executa somente os testes unitários e não exige infraestrutura. bun run test:all inclui integração e, portanto, exige PostgreSQL e LocalStack ativos.
Para iniciar a API pelo host, copie .env.example para .env, inicie as dependências com docker compose up -d --build postgres localstack, aplique bun run migration:run e execute bun run start:dev. O exemplo usa os endereços locais; o Compose fornece automaticamente os endereços internos aos containers. As credenciais wager e test servem apenas para a infraestrutura local do desafio.
Os testes de integração usam PostgreSQL e LocalStack reais. Eles recriam apenas o schema do banco wagering_test:
docker compose up -d postgres localstack
bun run test:integrationA suíte usa filas *-test.fifo próprias e filas temporárias reliability-*.fifo, removidas ao terminar. Pode rodar com os workers principais ativos. Não execute duas suítes de integração simultaneamente: ambas recriam o schema de wagering_test.
Alternativamente, rode tudo em container:
docker compose up -d postgres localstack
docker compose --profile test build integration-tests
docker compose --profile test run --rm --no-deps integration-testsA suíte cobre, entre outros casos:
- 50 entregas paralelas da mesma aposta produzindo um débito;
- duas apostas de
80.00disputando saldo100.00; - wallets distintas em paralelo;
LOSS,WIN,REFUNDeROLLBACK;- reversão que produziria saldo negativo;
- inbox e redelivery real no SQS;
- dois publishers concorrentes;
- mensagem permanente enviada à DLQ;
- referência entregue fora de ordem;
- três processos e morte depois do commit, antes do ACK;
- constraints de saldo e imutabilidade do ledger;
- ciclo automatizado de migration
up → down → up; - rollback integral quando a escrita da outbox falha;
- expiração de referência pendente e evento de rejeição;
- igualdade final entre saldo e ledger em cada caso de integração;
- disputa e 50 duplicatas distribuídas entre três processos reais de API;
- reserva expirada com dois workers de referência concorrentes;
- reconciliação durante um commit financeiro de outra conexão;
- retry de conexão PostgreSQL e redrive automático para a DLQ;
- indisponibilidade SQS, backoff e recuperação da outbox;
- morte/reinício de publisher após claim e após envio, antes da confirmação;
- idempotência persistente do consumidor de eventos republicados;
- SIGTERM real, devolução de visibility e ACK pela próxima instância;
- falha permanente persistida como FAILED, sem ledger nem alteração de saldo;
- métricas exportadas pelos workers e lag durante backoff;
- paginação estável durante novos movimentos e rejeição HTTP 400 de cursores inválidos ou fora do limite do banco.
Além de comparar saldo com ledger, os testes verificam a quantidade de lançamentos por status e a ausência de eventos terminais contraditórios.
Crie uma wallet usando UUIDs válidos:
curl -X POST http://localhost:3000/wallets \
-H 'Content-Type: application/json' \
-d '{
"playerId":"0192f28f-5dc0-7d58-bdb2-814ad6a0f4a1",
"initialBalance":{"amount":"100.00","currency":"BRL"}
}'Submeta uma aposta substituindo walletId pelo ID retornado:
curl -X POST http://localhost:3000/wagering/transactions \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: provider-a:transaction-123' \
-d '{
"providerId":"provider-a",
"externalTransactionId":"transaction-123",
"playerId":"0192f28f-5dc0-7d58-bdb2-814ad6a0f4a1",
"walletId":"WALLET_ID",
"roundId":"round-987",
"gameId":"fortune-chimp",
"kind":"BET",
"money":{"amount":"25.00","currency":"BRL"}
}'Repetir a chamada retorna o mesmo transactionId e o mesmo snapshot de saldo, com idempotentReplay: true.
POST /wallets
GET /wallets/:walletId
GET /wallets/:walletId/ledger?cursor=...&limit=50
POST /wallets/:walletId/reconciliation
POST /wagering/transactions
GET /wagering/transactions/:transactionId
GET /providers/:providerId/wagering/transactions/:externalTransactionId
GET /health/live
GET /health/ready
GET /metrics
docker compose run --rm migrate
docker compose run --rm migrate bun run migration:revertAs migrations não são executadas por cada réplica. No Compose, o job migrate termina antes da API e dos workers iniciarem. Para executá-las pelo host, configure DATABASE_URL=postgres://wager:wager@localhost:55432/wagering antes de usar bun run migration:run ou bun run migration:revert.
Filas criadas no startup do LocalStack:
wager-transactions.fifo
wager-transactions-dlq.fifo
wager-events.fifo
Envelope de entrada:
{
"messageId": "msg-123",
"type": "WagerTransactionRequested",
"occurredAt": "2026-07-29T15:00:00.000Z",
"data": {
"providerId": "provider-a",
"externalTransactionId": "transaction-123",
"idempotencyKey": "provider-a:transaction-123",
"playerId": "0192f28f-5dc0-7d58-bdb2-814ad6a0f4a1",
"walletId": "0192f291-27dd-7d3f-8071-5f8685deef37",
"roundId": "round-987",
"gameId": "fortune-chimp",
"kind": "BET",
"money": { "amount": "25.00", "currency": "BRL" }
}
}Use MessageGroupId = walletId. Isso reduz contenção, mas as garantias finais permanecem no PostgreSQL.
- logs JSON sem payload financeiro completo;
x-correlation-idaceito e devolvido pela API;- métricas Prometheus em
/metrics; - liveness separado de readiness;
- readiness consulta PostgreSQL e SQS.
Os workers também expõem /metrics, /health/live e /health/ready na porta interna 9000 (METRICS_PORT). Cada réplica tem uma porta local aleatória. Descubra o endereço de uma réplica com:
docker compose port --index 1 wager-consumer 9000
docker compose port --index 1 outbox-publisher 9000
docker compose port --index 1 reference-worker 9000Consulte http://ENDERECO_RETORNADO/metrics. Um coletor deve consultar cada réplica; os contadores dos workers não são agregados pela API. O lag e a profundidade da DLQ são consultados diretamente nas dependências e incluem backoff/redrive automático.
As decisões e garantias estão detalhadas em ARCHITECTURE.md. O mapeamento requisito a requisito está em COMPLIANCE.md.