Documento baseado no código-fonte atual do repositório. O conteúdo abaixo descreve apenas o que está implementado.
A aplicação é um sistema fullstack para transferências internas entre usuários.
O backend usa NestJS, TypeORM, PostgreSQL, Redis, autenticação JWT, Swagger e validação global.
O frontend usa Vue 3, TypeScript, Pinia, Vue Router, Axios, Zod e Tailwind CSS.
- autentica usuários com
POST /auth/login - encerra sessão e invalida o JWT no servidor com
POST /auth/logout - cadastra novos usuários com
POST /users - exibe o saldo da conta autenticada em
GET /wallet/balance - realiza transferências entre usuários em
POST /wallet/transfer - lista o histórico de transações em
GET /wallet/transactions - protege a rota de transferência com
Idempotency-Key - expõe documentação Swagger em
/api/docs
.
├── README.md
├── ANALISE_APLICACAO.md
├── docker-compose.yml
├── backend/
│ └── src/
│ ├── main.ts
│ ├── app.module.ts
│ ├── shared/
│ │ ├── auth/
│ │ ├── constants/
│ │ ├── domain/
│ │ ├── http/
│ │ ├── redis/
│ │ └── security/
│ ├── modules/
│ │ ├── auth/
│ │ ├── users/
│ │ └── wallet/
│ └── infrastructure/
│ └── database/
│ ├── seeds/
│ └── typeorm/
└── frontend/
└── src/
├── main.ts
├── App.vue
├── router/
├── stores/
├── services/
├── validation/
├── views/
├── components/
└── types/
O bootstrap está em backend/src/main.ts. Ele:
- cria a aplicação Nest
- habilita
CORSpara lista origen explícita - aplica validação global com
ValidationPipe - registra
DomainExceptionFilter - expõe Swagger em
/api/docs - sobe a aplicação na porta
3000por padrão
AuthModuletrata login e JWTUsersModuletrata cadastro de usuáriosWalletModuletrata saldo, transferência e históricoRedisModulefornece o client Redis de forma global
O AppModule conecta no PostgreSQL via TypeOrmModule.forRoot.
As entidades registradas são UserEntity, AccountEntity e TransactionEntity.
As migrations mostram a estrutura real do banco:
accountscombalance numeric(18,4)userscomusername,passwordeaccount_idtransactionscomdebited_account_id,credited_account_id,value numeric(18,4)ecreated_at- índices em
transactionspordebited_account_id + created_atecredited_account_id + created_at
LoginUseCasevalida usuário e senha e gera JWT comsub,username,accountIdejtiLogoutUseCaserecebejtieexpiresAt, calcula o TTL restante e bloqueia o token no RedisRegisterUserUseCasecria usuário e conta em uma transação no bancoGetBalanceUseCasebusca a conta e formata o saldo para exibiçãoCreateTransferUseCasevalida remetente, destinatário, saldo e executa a transferênciaListTransactionsUseCasemonta o histórico paginado com tipocash-inoucash-out
O código usa Repository Pattern com interfaces de domínio e implementações TypeORM:
UserRepositoryAccountRepositoryTransactionRepository
Usernamenormaliza para minúsculas, remove espaços nas bordas e exige no mínimo 3 caracteresPlainPasswordexige ao menos 8 caracteres, uma letra maiúscula e um númeroTransferAmountvalida valor maior que zero e com no máximo 4 casas decimaisMoneyrepresenta valores monetários com exatamente 4 casas decimaisBalancevalida saldo não negativo; expõedebit,crediteensureCanDebitPasswordHashvalida hash bcrypt nos prefixos$2a$,$2b$e$2y$UuidValueObjectvalida IDs UUID; subclassesAccountIdeUserIdadicionam o rótulo ao erroIdempotencyKeynormaliza para minúsculas e remove espaços nas bordasDomainErrore suas variações são mapeadas peloDomainExceptionFilter
O frontend é uma SPA com:
Vue Routerpara navegaçãoPiniapara estado de autenticaçãoAxiospara comunicação com a APIZodpara validação dos formulários
LoginView.vueautentica o usuário e salvatokeneusernamenolocalStorageRegisterView.vuecria conta e redireciona para loginDashboardView.vuemostra saldo, faz transferências e lista histórico
api.tscentraliza o client HTTP e injetaAuthorization: Bearer <token>auth-service.tschama login, cadastro e logoutwallet-service.tschama saldo, histórico e transferênciaidempotency.tsmonta a chave composta da transferênciamoney.tsnormaliza e formata valores monetários
- o store
authcontrolatoken,username, login, cadastro e logout - a rota
/dashboardfaz apenas uma checagem de existência de token nolocalStorage
- o usuário envia
usernameepasswordparaPOST /auth/login - o backend valida as credenciais e retorna
accessToken - o frontend salva o token no
localStorage - o usuário é redirecionado para
/dashboard
- o usuário envia
usernameepasswordparaPOST /users - o backend verifica unicidade do username
- a senha é hashada com
bcryptjsusando 10 rounds - conta e usuário são criados em transação
- o usuário clica em "Sair" no dashboard
- o frontend chama
POST /auth/logoutcom o token no header - o backend invalida o JTI no Redis e retorna a mensagem de confirmação
- somente após a resposta o frontend remove
tokeneusernamedolocalStorage - o frontend redireciona para
/logincom a mensagem recebida do backend
- o frontend envia
username,valueeIdempotency-Key - o backend valida token JWT, destinatário, valor e saldo
- a transferência é persistida em transação de banco
- o frontend recarrega saldo e histórico
- o dashboard carrega histórico automaticamente
- o frontend usa
limitfixo de5itens por página - os filtros disponíveis na interface são data inicial, data final, tipo e ordem
- a API também aceita
pageelimit
O Redis é usado em dois contextos independentes.
O RedisService implementa a interface TokenBlocklist.
LogoutUseCasegravablocklist:jti:<jti>com TTL igual ao tempo restante do tokenJwtStrategy.validate()consulta essa chave a cada requisição autenticada e lança401se o token estiver bloqueado
O IdempotencyInterceptor usa o mesmo RedisService para controle de requisições duplicadas.
O fluxo implementado é:
- ler o header
Idempotency-Key - exigir esse header em
POST /wallet/transfer - normalizar a chave para minúsculas
- negar requisições duplicadas com
409 Conflict - gravar um marcador
processingenquanto a transferência está em andamento - salvar a resposta final com TTL
Na prática:
- a aplicação não retorna o payload previamente salvo
- ela apenas bloqueia a reutilização da mesma chave enquanto o registro existir no Redis
- o TTL é lido de
IDEMPOTENCY_TTL_SECONDS, com fallback efetivo para5segundos se o valor não for numérico válido - no frontend, a chave enviada por padrão usa o componente final fixo
10, salvo se outro valor vier de variável de ambiente
O código trata valores monetários com precisão decimal.
Isso aparece em vários pontos:
- banco com
numeric(18,4) - value objects
Money,TransferAmounteBalance decimal.jsno backend e no seed- normalização de entrada no frontend
- formatação de saída para 2 casas decimais na interface
Observação prática:
- a API aceita até 4 casas decimais
- o formulário do frontend limita a entrada a 2 casas decimais
O backend usa um mecanismo interno de eventos de domínio.
AggregateRoot acumula eventos durante a execução de um caso de uso. NestDomainEventPublisher despacha esses eventos para os handlers registrados após cada operação.
Eventos implementados:
UserRegistered— publicado porRegisterUserUseCaseapós criar o usuário; consumido porUserRegisteredHandler, que loga o eventoTransferExecuted— publicado porCreateTransferUseCaseapós concluir a transferência; consumido porTransferAuditHandler, que loga o evento
Os handlers se registram no publisher via onModuleInit.
O seed atual:
- roda automaticamente no container do backend
- também pode ser executado manualmente com
npm run seed - cria usuários apenas se a base estiver vazia
- cria 10 usuários com senha
Senha123 - cria saldo inicial fixo de
100.0000para cada conta - segue a mesma regra obrigatória de domínio usada no fluxo real de cadastro
- gera 20 transações entre esses usuários
O backend usa Jest com cobertura coletada sobre casos de uso, value objects e interceptors.
Arquivos de spec presentes:
- casos de uso:
login,logout,register-user,get-balance,create-transfer,list-transactions - domínio:
TransferDomainService - value objects:
Username,Balance,Money,TransferAmount,InitialBalance,PlainPassword,PasswordHash,UuidValueObject(cobreAccountIdeUserId),IdempotencyKey - handlers de eventos:
UserRegisteredHandler,TransferAuditHandler - infraestrutura de eventos:
NestDomainEventPublisher - repositórios TypeORM:
TypeOrmAccountRepository,TypeOrmTransactionRepository,TypeOrmUserRepository - filtro:
DomainExceptionFilter - interceptor:
IdempotencyInterceptor - testes HTTP de controladores via
createHttpTestApp()(Fastify real, repositórios e Redis fakes)
Execute com:
cd backend
npm testO frontend usa Vitest com ambiente jsdom.
Arquivos de spec presentes:
src/services/money.spec.ts—normalizeMoneyInputeformatMoneyForDisplay(arredondamento bancário)src/validation/forms.spec.ts—loginFormSchema,registerFormSchema,transferFormSchemaegetFirstValidationMessagesrc/services/idempotency.spec.ts—buildTransferIdempotencyKey
Execute com:
cd frontend
npm test