38
.env
Normal file
38
.env
Normal file
@@ -0,0 +1,38 @@
|
||||
ORACLE_HOST=10.1.1.241
|
||||
ORACLE_CONNECT_STRING= (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = 10.1.1.241)(PORT = 1521)))(CONNECT_DATA = (SERVICE_NAME = WINT)))
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SERVICE=WINT
|
||||
ORACLE_USER=SEVEN
|
||||
ORACLE_PASSWORD=USR54SEV
|
||||
|
||||
ORACLE_POOL_MIN=5
|
||||
ORACLE_POOL_MAX=20
|
||||
ORACLE_POOL_INCREMENT=5
|
||||
ORACLE_POOL_TIMEOUT=30000
|
||||
ORACLE_POOL_IDLE_TIMEOUT=300000
|
||||
|
||||
POSTGRES_HOST=10.1.1.222
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USER=ti
|
||||
POSTGRES_PASSWORD=ti
|
||||
POSTGRES_DB=ksdb
|
||||
|
||||
POSTGRES_POOL_MIN=5
|
||||
POSTGRES_POOL_MAX=20
|
||||
POSTGRES_POOL_IDLE_TIMEOUT=30000
|
||||
POSTGRES_POOL_CONNECTION_TIMEOUT=5000
|
||||
POSTGRES_POOL_ACQUIRE_TIMEOUT=60000
|
||||
|
||||
JWT_SECRET=4557C0D7-DFB0-40DA-BF83-91A75103F7A9
|
||||
JWT_EXPIRES_IN=8h
|
||||
|
||||
THROTTLE_TTL=60
|
||||
THROTTLE_LIMIT=10
|
||||
|
||||
NODE_ENV=development
|
||||
|
||||
ORACLE_CLIENT_LIB_DIR=C:\\instantclient_19_25
|
||||
|
||||
|
||||
|
||||
|
||||
39
.env.example
Normal file
39
.env.example
Normal file
@@ -0,0 +1,39 @@
|
||||
# Configuração do Oracle
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_CONNECT_STRING=(DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521)))(CONNECT_DATA = (SERVICE_NAME = DBNAME)))
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SERVICE=DBNAME
|
||||
ORACLE_USER=user
|
||||
ORACLE_PASSWORD=password
|
||||
|
||||
# Configuração de Pool Oracle
|
||||
ORACLE_POOL_MIN=5
|
||||
ORACLE_POOL_MAX=20
|
||||
ORACLE_POOL_INCREMENT=5
|
||||
ORACLE_POOL_TIMEOUT=30000
|
||||
ORACLE_POOL_IDLE_TIMEOUT=300000
|
||||
|
||||
# Configuração do PostgreSQL
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USER=user
|
||||
POSTGRES_PASSWORD=password
|
||||
POSTGRES_DB=dbname
|
||||
|
||||
# Configuração de Pool PostgreSQL
|
||||
POSTGRES_POOL_MIN=5
|
||||
POSTGRES_POOL_MAX=20
|
||||
POSTGRES_POOL_IDLE_TIMEOUT=30000
|
||||
POSTGRES_POOL_CONNECTION_TIMEOUT=5000
|
||||
POSTGRES_POOL_ACQUIRE_TIMEOUT=60000
|
||||
|
||||
# Configuração JWT
|
||||
JWT_SECRET=your-secret-jwt-key-here
|
||||
JWT_EXPIRES_IN=8h
|
||||
|
||||
# Rate Limiting
|
||||
THROTTLE_TTL=60
|
||||
THROTTLE_LIMIT=10
|
||||
|
||||
# Ambiente
|
||||
NODE_ENV=development
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -32,3 +32,9 @@ lerna-debug.log*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.env.prod
|
||||
.env.staging
|
||||
.env.local
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"compile-hero.disable-compile-files-on-did-save-code": false
|
||||
}
|
||||
300
API.md
Normal file
300
API.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# Portal Juru API Documentation
|
||||
|
||||
## Índice
|
||||
1. [Autenticação](#autenticação)
|
||||
2. [Endpoints](#endpoints)
|
||||
- [Consulta de Dados](#consulta-de-dados)
|
||||
- [Pedidos e Pagamentos](#pedidos-e-pagamentos)
|
||||
3. [Modelos de Dados](#modelos-de-dados)
|
||||
4. [Exemplos de Uso](#exemplos-de-uso)
|
||||
5. [Códigos de Erro](#códigos-de-erro)
|
||||
|
||||
## Autenticação
|
||||
|
||||
A API utiliza autenticação JWT. Para acessar os endpoints protegidos, inclua o token no header:
|
||||
|
||||
```
|
||||
Authorization: Bearer seu-token-jwt
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Consulta de Dados
|
||||
|
||||
#### Listar Lojas
|
||||
```http
|
||||
GET /api/v1/data-consult/stores
|
||||
```
|
||||
|
||||
**Resposta**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "001",
|
||||
"name": "Loja Principal",
|
||||
"store": "001 - Loja Principal"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Listar Vendedores
|
||||
```http
|
||||
GET /api/v1/data-consult/sellers
|
||||
```
|
||||
|
||||
**Resposta**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "001",
|
||||
"name": "João Silva"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Consultar Faturamento
|
||||
```http
|
||||
GET /api/v1/data-consult/billings
|
||||
```
|
||||
|
||||
**Resposta**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "001",
|
||||
"date": "2024-04-02T10:00:00Z",
|
||||
"total": 1000.00
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Filtrar Clientes
|
||||
```http
|
||||
GET /api/v1/data-consult/customers/:filter
|
||||
```
|
||||
|
||||
**Parâmetros**
|
||||
- `filter`: Termo de busca (nome, documento, etc.)
|
||||
|
||||
**Resposta**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "001",
|
||||
"name": "Maria Silva",
|
||||
"document": "123.456.789-00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Buscar Produtos
|
||||
```http
|
||||
GET /api/v1/data-consult/products/:filter
|
||||
```
|
||||
|
||||
**Parâmetros**
|
||||
- `filter`: Termo de busca (nome, código, etc.)
|
||||
|
||||
**Resposta**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "001",
|
||||
"name": "Produto Exemplo",
|
||||
"manufacturerCode": "ABC123"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Pedidos e Pagamentos
|
||||
|
||||
#### Listar Pedidos da Loja
|
||||
```http
|
||||
GET /api/v1/orders-payment/orders/:id
|
||||
```
|
||||
|
||||
**Parâmetros**
|
||||
- `id`: ID da loja
|
||||
|
||||
**Resposta**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"createDate": "2024-04-02T10:00:00Z",
|
||||
"storeId": "001",
|
||||
"orderId": 12345,
|
||||
"customerId": "001",
|
||||
"customerName": "João Silva",
|
||||
"sellerId": "001",
|
||||
"sellerName": "Maria Santos",
|
||||
"billingId": "001",
|
||||
"billingName": "Cartão de Crédito",
|
||||
"planId": "001",
|
||||
"planName": "3x sem juros",
|
||||
"amount": 1000.00,
|
||||
"installments": 3,
|
||||
"amountPaid": 1000.00
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Buscar Pedido Específico
|
||||
```http
|
||||
GET /api/v1/orders-payment/orders/:id/:orderId
|
||||
```
|
||||
|
||||
**Parâmetros**
|
||||
- `id`: ID da loja
|
||||
- `orderId`: ID do pedido
|
||||
|
||||
**Resposta**
|
||||
```json
|
||||
{
|
||||
"createDate": "2024-04-02T10:00:00Z",
|
||||
"storeId": "001",
|
||||
"orderId": 12345,
|
||||
"customerId": "001",
|
||||
"customerName": "João Silva",
|
||||
"sellerId": "001",
|
||||
"sellerName": "Maria Santos",
|
||||
"billingId": "001",
|
||||
"billingName": "Cartão de Crédito",
|
||||
"planId": "001",
|
||||
"planName": "3x sem juros",
|
||||
"amount": 1000.00,
|
||||
"installments": 3,
|
||||
"amountPaid": 1000.00
|
||||
}
|
||||
```
|
||||
|
||||
#### Listar Pagamentos do Pedido
|
||||
```http
|
||||
GET /api/v1/orders-payment/payments/:id
|
||||
```
|
||||
|
||||
**Parâmetros**
|
||||
- `id`: ID do pedido
|
||||
|
||||
**Resposta**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"orderId": 12345,
|
||||
"payDate": "2024-04-02T10:00:00Z",
|
||||
"card": "**** **** **** 1234",
|
||||
"installments": 3,
|
||||
"flagName": "VISA",
|
||||
"type": "CREDITO",
|
||||
"amount": 1000.00,
|
||||
"userId": "001",
|
||||
"nsu": "123456789",
|
||||
"auth": "A12345"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Criar Pagamento
|
||||
```http
|
||||
POST /api/v1/orders-payment/payments/create
|
||||
```
|
||||
|
||||
**Corpo da Requisição**
|
||||
```json
|
||||
{
|
||||
"orderId": 12345,
|
||||
"card": "**** **** **** 1234",
|
||||
"auth": "A12345",
|
||||
"nsu": "123456789",
|
||||
"installments": 3,
|
||||
"amount": 1000.00,
|
||||
"flagName": "VISA",
|
||||
"paymentType": "CREDITO",
|
||||
"userId": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### Criar Fatura
|
||||
```http
|
||||
POST /api/v1/orders-payment/invoice/create
|
||||
```
|
||||
|
||||
**Corpo da Requisição**
|
||||
```json
|
||||
{
|
||||
"orderId": 12345,
|
||||
"userId": 1
|
||||
}
|
||||
```
|
||||
|
||||
## Modelos de Dados
|
||||
|
||||
### OrderDto
|
||||
```typescript
|
||||
interface OrderDto {
|
||||
createDate: Date; // Data de criação do pedido
|
||||
storeId: string; // ID da loja
|
||||
orderId: number; // ID do pedido
|
||||
customerId: string; // ID do cliente
|
||||
customerName: string; // Nome do cliente
|
||||
sellerId: string; // ID do vendedor
|
||||
sellerName: string; // Nome do vendedor
|
||||
billingId: string; // ID da forma de pagamento
|
||||
billingName: string; // Nome da forma de pagamento
|
||||
planId: string; // ID do plano de pagamento
|
||||
planName: string; // Nome do plano de pagamento
|
||||
amount: number; // Valor total do pedido
|
||||
installments: number; // Número de parcelas
|
||||
amountPaid: number; // Valor total pago
|
||||
}
|
||||
```
|
||||
|
||||
### PaymentDto
|
||||
```typescript
|
||||
interface PaymentDto {
|
||||
orderId: number; // ID do pedido
|
||||
payDate: Date; // Data do pagamento
|
||||
card: string; // Número do cartão
|
||||
installments: number; // Número de parcelas
|
||||
flagName: string; // Nome da bandeira
|
||||
type: string; // Tipo de pagamento
|
||||
amount: number; // Valor do pagamento
|
||||
userId: string; // ID do usuário
|
||||
nsu: string; // NSU da transação
|
||||
auth: string; // Código de autorização
|
||||
}
|
||||
```
|
||||
|
||||
## Códigos de Erro
|
||||
|
||||
| Código | Descrição |
|
||||
|--------|-----------|
|
||||
| 400 | Requisição inválida |
|
||||
| 401 | Não autorizado |
|
||||
| 403 | Acesso negado |
|
||||
| 404 | Recurso não encontrado |
|
||||
| 500 | Erro interno do servidor |
|
||||
|
||||
## Exemplos de Uso
|
||||
|
||||
### Exemplo de Requisição com cURL
|
||||
|
||||
```bash
|
||||
# Listar pedidos de uma loja
|
||||
curl -X GET "http://localhost:3000/api/v1/orders-payment/orders/001" \
|
||||
-H "Authorization: Bearer seu-token-jwt"
|
||||
|
||||
# Criar novo pagamento
|
||||
curl -X POST "http://localhost:3000/api/v1/orders-payment/payments/create" \
|
||||
-H "Authorization: Bearer seu-token-jwt" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"orderId": 12345,
|
||||
"card": "**** **** **** 1234",
|
||||
"auth": "A12345",
|
||||
"nsu": "123456789",
|
||||
"installments": 3,
|
||||
"amount": 1000.00,
|
||||
"flagName": "VISA",
|
||||
"paymentType": "CREDITO",
|
||||
"userId": 1
|
||||
}'
|
||||
141
CONTRIBUTING.md
Normal file
141
CONTRIBUTING.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Guia de Contribuição
|
||||
|
||||
Obrigado por considerar contribuir para o Portal Juru API! Este documento fornece um conjunto de diretrizes para contribuir com o projeto.
|
||||
|
||||
## Como Contribuir
|
||||
|
||||
1. Faça um fork do projeto
|
||||
2. Crie uma branch para sua feature (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push para a branch (`git push origin feature/AmazingFeature`)
|
||||
5. Abra um Pull Request
|
||||
|
||||
## Padrões de Código
|
||||
|
||||
### TypeScript/JavaScript
|
||||
|
||||
- Use TypeScript para todo o código
|
||||
- Siga o [style guide oficial do TypeScript](https://www.typescriptlang.org/docs/handbook/declaration-files/by-example.html)
|
||||
- Use ESLint e Prettier para formatação
|
||||
- Mantenha o código limpo e bem documentado
|
||||
|
||||
### NestJS
|
||||
|
||||
- Siga as [melhores práticas do NestJS](https://docs.nestjs.com/recipes/prisma)
|
||||
- Use decorators apropriadamente
|
||||
- Mantenha os módulos bem organizados
|
||||
- Use DTOs para validação de dados
|
||||
|
||||
### Banco de Dados
|
||||
|
||||
- Use TypeORM para todas as operações de banco de dados
|
||||
- Mantenha as queries SQL otimizadas
|
||||
- Use transações quando necessário
|
||||
- Documente as queries complexas
|
||||
|
||||
## Estrutura do Projeto
|
||||
|
||||
```
|
||||
src/
|
||||
├── core/ # Configurações e utilitários core
|
||||
│ ├── configs/ # Configurações do projeto
|
||||
│ ├── database/ # Configuração do banco de dados
|
||||
│ └── constants/ # Constantes do sistema
|
||||
├── data-consult/ # Módulo de consulta de dados
|
||||
│ ├── dto/ # Data Transfer Objects
|
||||
│ ├── controllers/ # Controladores
|
||||
│ └── services/ # Serviços
|
||||
├── orders-payment/ # Módulo de pedidos e pagamentos
|
||||
│ ├── dto/ # Data Transfer Objects
|
||||
│ ├── controllers/ # Controladores
|
||||
│ └── services/ # Serviços
|
||||
└── orders/ # Módulo de pedidos
|
||||
├── modules/ # Módulos
|
||||
├── controllers/ # Controladores
|
||||
├── services/ # Serviços
|
||||
└── repositories/ # Repositórios
|
||||
```
|
||||
|
||||
## Convenções de Commits
|
||||
|
||||
Use o seguinte formato para commits:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
Tipos de commit:
|
||||
- `feat`: Nova feature
|
||||
- `fix`: Correção de bug
|
||||
- `docs`: Mudanças na documentação
|
||||
- `style`: Formatação, ponto e vírgula, etc.
|
||||
- `refactor`: Refatoração de código
|
||||
- `test`: Adição ou correção de testes
|
||||
- `chore`: Atualização de tarefas, configuração, etc.
|
||||
|
||||
Exemplo:
|
||||
```
|
||||
feat(orders): adiciona endpoint para criar pedidos
|
||||
|
||||
- Implementa validação de dados
|
||||
- Adiciona testes unitários
|
||||
- Atualiza documentação
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
## Testes
|
||||
|
||||
- Mantenha a cobertura de testes acima de 80%
|
||||
- Use Jest para testes
|
||||
- Escreva testes unitários e de integração
|
||||
- Execute todos os testes antes de submeter um PR
|
||||
|
||||
```bash
|
||||
# Executar testes
|
||||
npm run test
|
||||
|
||||
# Executar testes com cobertura
|
||||
npm run test:cov
|
||||
```
|
||||
|
||||
## Documentação
|
||||
|
||||
- Mantenha a documentação atualizada
|
||||
- Use Swagger para documentação da API
|
||||
- Documente todas as novas features
|
||||
- Atualize o README quando necessário
|
||||
|
||||
## Pull Requests
|
||||
|
||||
1. Descreva claramente as mudanças
|
||||
2. Inclua exemplos de uso quando relevante
|
||||
3. Atualize a documentação
|
||||
4. Certifique-se que todos os testes passam
|
||||
5. Solicite revisão de pelo menos um mantenedor
|
||||
|
||||
## Issues
|
||||
|
||||
- Use o template apropriado
|
||||
- Forneça informações detalhadas
|
||||
- Inclua exemplos de reprodução quando possível
|
||||
- Use labels apropriadas
|
||||
|
||||
## Código de Conduta
|
||||
|
||||
- Seja respeitoso
|
||||
- Mantenha discussões construtivas
|
||||
- Aceite críticas construtivas
|
||||
- Ajude outros contribuidores
|
||||
|
||||
## Suporte
|
||||
|
||||
Se você tiver dúvidas ou precisar de ajuda:
|
||||
- Abra uma issue
|
||||
- Entre em contato com os mantenedores
|
||||
- Consulte a documentação
|
||||
|
||||
190
README.md
190
README.md
@@ -1,73 +1,157 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
# Portal Juru API
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
API para gerenciamento de pedidos, pagamentos e consultas de dados do sistema Portal Juru.
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
## 🚀 Tecnologias
|
||||
|
||||
## Description
|
||||
- [NestJS](https://nestjs.com/) - Framework Node.js
|
||||
- [TypeORM](https://typeorm.io/) - ORM para banco de dados
|
||||
- [Swagger](https://swagger.io/) - Documentação da API
|
||||
- [Oracle Database](https://www.oracle.com/database/) - Banco de dados
|
||||
- [Redis](https://redis.io/) - Cache em memória
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
## 📋 Pré-requisitos
|
||||
|
||||
## Installation
|
||||
- Node.js (v16 ou superior)
|
||||
- Oracle Database
|
||||
- Redis
|
||||
- npm ou yarn
|
||||
|
||||
## 🔧 Instalação
|
||||
|
||||
1. Clone o repositório:
|
||||
```bash
|
||||
$ npm install
|
||||
git clone https://github.com/seu-usuario/portaljuru-api.git
|
||||
cd portaljuru-api
|
||||
```
|
||||
|
||||
## Running the app
|
||||
|
||||
2. Instale as dependências:
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
npm install
|
||||
# ou
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
3. Configure as variáveis de ambiente:
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
cp .env.example .env
|
||||
# Edite o arquivo .env com suas configurações
|
||||
```
|
||||
|
||||
## Support
|
||||
4. Inicie o servidor:
|
||||
```bash
|
||||
npm run start:dev
|
||||
# ou
|
||||
yarn start:dev
|
||||
```
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
## 📚 Documentação da API
|
||||
|
||||
## Stay in touch
|
||||
A documentação completa da API está disponível em `/api` quando o servidor estiver rodando.
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
### Endpoints Principais
|
||||
|
||||
## License
|
||||
#### Consulta de Dados
|
||||
- `GET /api/v1/data-consult/stores` - Lista todas as lojas
|
||||
- `GET /api/v1/data-consult/sellers` - Lista todos os vendedores
|
||||
- `GET /api/v1/data-consult/billings` - Retorna informações de faturamento
|
||||
- `GET /api/v1/data-consult/customers/:filter` - Filtra clientes
|
||||
- `GET /api/v1/data-consult/products/:filter` - Busca produtos filtrados
|
||||
|
||||
Nest is [MIT licensed](LICENSE).
|
||||
#### Pedidos e Pagamentos
|
||||
- `GET /api/v1/orders-payment/orders/:id` - Lista pedidos de uma loja
|
||||
- `GET /api/v1/orders-payment/orders/:id/:orderId` - Busca pedido específico
|
||||
- `GET /api/v1/orders-payment/payments/:id` - Lista pagamentos de um pedido
|
||||
- `POST /api/v1/orders-payment/payments/create` - Cria novo pagamento
|
||||
- `POST /api/v1/orders-payment/invoice/create` - Cria nova fatura
|
||||
|
||||
## 🛠️ Estrutura do Projeto
|
||||
|
||||
```
|
||||
src/
|
||||
├── core/ # Configurações e utilitários core
|
||||
│ ├── configs/ # Configurações do projeto
|
||||
│ ├── database/ # Configuração do banco de dados
|
||||
│ └── constants/ # Constantes do sistema
|
||||
├── data-consult/ # Módulo de consulta de dados
|
||||
│ ├── dto/ # Data Transfer Objects
|
||||
│ ├── controllers/ # Controladores
|
||||
│ └── services/ # Serviços
|
||||
├── orders-payment/ # Módulo de pedidos e pagamentos
|
||||
│ ├── dto/ # Data Transfer Objects
|
||||
│ ├── controllers/ # Controladores
|
||||
│ └── services/ # Serviços
|
||||
└── orders/ # Módulo de pedidos
|
||||
├── modules/ # Módulos
|
||||
├── controllers/ # Controladores
|
||||
├── services/ # Serviços
|
||||
└── repositories/ # Repositórios
|
||||
```
|
||||
|
||||
## 🔐 Autenticação
|
||||
|
||||
A API utiliza autenticação JWT. Para acessar os endpoints protegidos, inclua o token no header:
|
||||
|
||||
```
|
||||
Authorization: Bearer seu-token-jwt
|
||||
```
|
||||
|
||||
## 📦 DTOs (Data Transfer Objects)
|
||||
|
||||
### OrderDto
|
||||
```typescript
|
||||
{
|
||||
createDate: Date; // Data de criação do pedido
|
||||
storeId: string; // ID da loja
|
||||
orderId: number; // ID do pedido
|
||||
customerId: string; // ID do cliente
|
||||
customerName: string; // Nome do cliente
|
||||
sellerId: string; // ID do vendedor
|
||||
sellerName: string; // Nome do vendedor
|
||||
billingId: string; // ID da forma de pagamento
|
||||
billingName: string; // Nome da forma de pagamento
|
||||
planId: string; // ID do plano de pagamento
|
||||
planName: string; // Nome do plano de pagamento
|
||||
amount: number; // Valor total do pedido
|
||||
installments: number; // Número de parcelas
|
||||
amountPaid: number; // Valor total pago
|
||||
}
|
||||
```
|
||||
|
||||
### PaymentDto
|
||||
```typescript
|
||||
{
|
||||
orderId: number; // ID do pedido
|
||||
payDate: Date; // Data do pagamento
|
||||
card: string; // Número do cartão
|
||||
installments: number; // Número de parcelas
|
||||
flagName: string; // Nome da bandeira
|
||||
type: string; // Tipo de pagamento
|
||||
amount: number; // Valor do pagamento
|
||||
userId: string; // ID do usuário
|
||||
nsu: string; // NSU da transação
|
||||
auth: string; // Código de autorização
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testes
|
||||
|
||||
Para executar os testes:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
# ou
|
||||
yarn test
|
||||
```
|
||||
|
||||
## 📝 Licença
|
||||
|
||||
Este projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes.
|
||||
|
||||
## 🤝 Contribuição
|
||||
|
||||
1. Faça o fork do projeto
|
||||
2. Crie uma branch para sua feature (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push para a branch (`git push origin feature/AmazingFeature`)
|
||||
5. Abra um Pull Request
|
||||
|
||||
120
db-connection-pool.md
Normal file
120
db-connection-pool.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Configuração de Pool de Conexões
|
||||
|
||||
Este documento descreve a configuração do pool de conexões implementada para os bancos de dados Oracle e PostgreSQL na aplicação.
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O pool de conexões é uma técnica que mantém um conjunto de conexões abertas com o banco de dados, permitindo seu reuso entre diferentes requisições. Isso traz diversos benefícios:
|
||||
|
||||
- **Melhor performance**: Eliminação do overhead de abertura e fechamento de conexões
|
||||
- **Melhor escalabilidade**: Gerenciamento eficiente do número máximo de conexões
|
||||
- **Maior resiliência**: Tratamento de falhas de conexão e reconexão automática
|
||||
- **Menor carga no banco de dados**: Menor número de operações de login/logout
|
||||
|
||||
## Configuração do Oracle
|
||||
|
||||
### Parâmetros Configuráveis
|
||||
|
||||
Os seguintes parâmetros podem ser configurados através do arquivo `.env`:
|
||||
|
||||
| Parâmetro | Descrição | Valor Padrão |
|
||||
|-----------|-----------|--------------|
|
||||
| `ORACLE_POOL_MIN` | Número mínimo de conexões no pool | 5 |
|
||||
| `ORACLE_POOL_MAX` | Número máximo de conexões no pool | 20 |
|
||||
| `ORACLE_POOL_INCREMENT` | Incremento no número de conexões quando necessário | 5 |
|
||||
| `ORACLE_POOL_TIMEOUT` | Tempo máximo (ms) para obter uma conexão | 30000 |
|
||||
| `ORACLE_POOL_IDLE_TIMEOUT` | Tempo máximo (ms) que uma conexão pode ficar inativa | 300000 |
|
||||
|
||||
### Configurações Adicionais
|
||||
|
||||
Além dos parâmetros acima, as seguintes configurações foram implementadas:
|
||||
|
||||
- **Statement Cache**: Cache de 30 statements para melhorar a performance de queries repetidas
|
||||
- **Connection Class**: Identificador 'PORTALJURU' para rastrear conexões no banco
|
||||
- **Pool Ping Interval**: Verificação a cada 60 segundos para manter conexões ativas
|
||||
- **Enable Stats**: Habilitação de estatísticas para monitoramento do pool
|
||||
|
||||
## Configuração do PostgreSQL
|
||||
|
||||
### Parâmetros Configuráveis
|
||||
|
||||
Os seguintes parâmetros podem ser configurados através do arquivo `.env`:
|
||||
|
||||
| Parâmetro | Descrição | Valor Padrão |
|
||||
|-----------|-----------|--------------|
|
||||
| `POSTGRES_POOL_MIN` | Número mínimo de conexões no pool | 5 |
|
||||
| `POSTGRES_POOL_MAX` | Número máximo de conexões no pool | 20 |
|
||||
| `POSTGRES_POOL_IDLE_TIMEOUT` | Tempo máximo (ms) que uma conexão pode ficar inativa | 30000 |
|
||||
| `POSTGRES_POOL_CONNECTION_TIMEOUT` | Tempo máximo (ms) para estabelecer uma conexão | 5000 |
|
||||
| `POSTGRES_POOL_ACQUIRE_TIMEOUT` | Tempo máximo (ms) para obter uma conexão do pool | 60000 |
|
||||
|
||||
### Configurações Adicionais
|
||||
|
||||
Além dos parâmetros acima, as seguintes configurações foram implementadas:
|
||||
|
||||
- **Statement Timeout**: Limite de 10 segundos para execução de queries
|
||||
- **Query Timeout**: Limite de 10 segundos para execução de queries
|
||||
- **SSL**: Configuração automática baseada no ambiente (development/production)
|
||||
- **Query Cache**: Cache de 60 segundos para resultados de consultas
|
||||
|
||||
## Validação de Valores
|
||||
|
||||
O sistema implementa validação rigorosa para garantir valores apropriados:
|
||||
|
||||
- **Conversão Explícita**: Todos os valores de configuração são explicitamente convertidos para números (parseInt)
|
||||
- **Valores Mínimos**: Cada parâmetro tem um valor mínimo aplicado automaticamente
|
||||
- poolMin: no mínimo 1
|
||||
- poolMax: no mínimo poolMin + 1
|
||||
- timeouts: no mínimo 1000ms
|
||||
- **Validação de Relações**: O sistema garante que poolMax seja sempre maior que poolMin
|
||||
- **Arredondamento**: Valores convertidos de milissegundos para segundos são arredondados (Math.floor)
|
||||
|
||||
Estas validações previnem erros comuns como:
|
||||
- "NJS-007: invalid value for poolMax"
|
||||
- Timeouts negativos ou muito baixos
|
||||
- Problemas de conversão entre strings e números
|
||||
|
||||
## Boas Práticas
|
||||
|
||||
### Dimensionamento do Pool
|
||||
|
||||
O dimensionamento do pool de conexões depende da carga esperada:
|
||||
|
||||
1. **Fórmula Básica**: `connections = ((core_count * 2) + effective_spindle_count)`
|
||||
2. **Ambiente Web**: Considere o número máximo de requisições concorrentes
|
||||
3. **Regra 80-20**: O pool deve ser dimensionado para acomodar 80% da carga de pico
|
||||
|
||||
### Monitoramento
|
||||
|
||||
Para garantir o funcionamento adequado do pool, monitore:
|
||||
|
||||
- **Taxa de uso do pool**: Número de conexões ativas vs. total
|
||||
- **Tempo de espera por conexão**: Tempo médio para obter uma conexão
|
||||
- **Erros de timeout**: Número de falhas por timeout
|
||||
- **Conexões mortas**: Conexões que falharam mas não foram removidas do pool
|
||||
|
||||
### Ajuste Fino
|
||||
|
||||
Para ambientes de produção, considere os seguintes ajustes:
|
||||
|
||||
1. **Tamanho do pool**: Ajuste baseado no número de requisições concorrentes
|
||||
2. **Tempo de idle**: Reduza em ambientes com muitos usuários esporádicos
|
||||
3. **Tempo de timeout**: Aumente em caso de operações mais longas
|
||||
4. **Statement cache**: Aumente para aplicações com queries repetitivas
|
||||
|
||||
## Implementação no TypeORM
|
||||
|
||||
A configuração foi implementada nos arquivos:
|
||||
|
||||
- `src/core/configs/typeorm.oracle.config.ts`
|
||||
- `src/core/configs/typeorm.postgres.config.ts`
|
||||
|
||||
Estas configurações são carregadas no início da aplicação e aplicadas a todas as conexões.
|
||||
|
||||
|
||||
|
||||
### Melhores Práticas para Diagnóstico
|
||||
|
||||
1. **Use o endpoint de health check**: Verifique estatísticas do pool em `/health/pool`
|
||||
2. **Analise logs**: Procure por padrões de erro relacionados a conexões
|
||||
3. **Monitore performance**: Observe tempos de resposta e correlacione com o uso do pool
|
||||
30
dependencias-atualizadas.md
Normal file
30
dependencias-atualizadas.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Atualizações de Dependências
|
||||
|
||||
## Dependências Atualizadas
|
||||
- `@nestjs/mapped-types`: de 1.0.0 para 2.1.0
|
||||
- `@nestjs/swagger`: de 7.4.2 para 11.1.0
|
||||
- `bullmq`: de 5.45.2 para 5.46.0
|
||||
- `oracledb`: de 5.5.0 para 6.8.0
|
||||
- `reflect-metadata`: de 0.1.14 para 0.2.2
|
||||
|
||||
## Dependências de Desenvolvimento Atualizadas
|
||||
- `@types/node`: para 22.14.0
|
||||
- `rimraf`: para 6.0.1
|
||||
|
||||
## Dependências Que Ainda Precisam Ser Atualizadas
|
||||
- Pacotes de teste (Jest): A atualização do Jest (de 26.x para 29.x) requer uma migração significativa e pode quebrar testes existentes.
|
||||
- Prettier e ESLint: Estes podem ser atualizados em uma fase posterior.
|
||||
|
||||
## Vulnerabilidades
|
||||
- Ainda existem 18 vulnerabilidades (16 moderadas, 2 altas) relacionadas principalmente ao Jest, que é usado apenas para testes.
|
||||
- Para resolver todas as vulnerabilidades, incluindo mudanças significativas, você poderia executar `npm audit fix --force`, mas isso poderia quebrar funcionalidades existentes.
|
||||
|
||||
## Próximos Passos Recomendados
|
||||
1. Atualizar o Jest separadamente, fazendo os ajustes necessários nos testes
|
||||
2. Atualizar o ESLint e Prettier para versões mais recentes
|
||||
3. Verificar a compatibilidade entre as dependências atualizadas e as que não foram atualizadas
|
||||
4. Considerar atualizações de TypeORM e outros pacotes específicos do domínio
|
||||
|
||||
## Observações
|
||||
- Usamos `--legacy-peer-deps` para contornar conflitos de dependências. Isso pode mascarar incompatibilidades reais entre pacotes.
|
||||
- Recomenda-se testar a aplicação extensivamente após essas atualizações.
|
||||
60
docker-compose.monitoring.yml
Normal file
60
docker-compose.monitoring.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./monitoring/prometheus/:/etc/prometheus/
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||
- '--web.console.templates=/etc/prometheus/consoles'
|
||||
- '--web.enable-lifecycle'
|
||||
ports:
|
||||
- "9090:9090"
|
||||
networks:
|
||||
- monitoring-network
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./monitoring/grafana/provisioning/:/etc/grafana/provisioning/
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- prometheus
|
||||
networks:
|
||||
- monitoring-network
|
||||
|
||||
alertmanager:
|
||||
image: prom/alertmanager:latest
|
||||
container_name: alertmanager
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./monitoring/alertmanager/:/etc/alertmanager/
|
||||
command:
|
||||
- '--config.file=/etc/alertmanager/config.yml'
|
||||
- '--storage.path=/alertmanager'
|
||||
ports:
|
||||
- "9093:9093"
|
||||
networks:
|
||||
- monitoring-network
|
||||
|
||||
networks:
|
||||
monitoring-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
prometheus_data:
|
||||
grafana_data:
|
||||
146
health-check.md
Normal file
146
health-check.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Health Check da API
|
||||
|
||||
## Descrição
|
||||
|
||||
O sistema de Health Check implementado permite monitorar a saúde da aplicação e seus componentes críticos, como bancos de dados, uso de memória e espaço em disco. Isso facilita a detecção precoce de problemas e ajuda a manter a estabilidade da aplicação.
|
||||
|
||||
## Endpoints Disponíveis
|
||||
|
||||
### Verificação Geral
|
||||
|
||||
- **URL**: `/health`
|
||||
- **Método**: `GET`
|
||||
- **Descrição**: Verifica a saúde geral da aplicação, incluindo:
|
||||
- Status da API (ping)
|
||||
- Uso de disco
|
||||
- Uso de memória
|
||||
- Conexões de banco de dados (Oracle e PostgreSQL)
|
||||
|
||||
### Verificação de Banco de Dados
|
||||
|
||||
- **URL**: `/health/db`
|
||||
- **Método**: `GET`
|
||||
- **Descrição**: Verifica apenas as conexões de banco de dados (Oracle e PostgreSQL)
|
||||
|
||||
### Verificação de Memória
|
||||
|
||||
- **URL**: `/health/memory`
|
||||
- **Método**: `GET`
|
||||
- **Descrição**: Verifica o uso de memória da aplicação (heap e RSS)
|
||||
|
||||
### Verificação de Disco
|
||||
|
||||
- **URL**: `/health/disk`
|
||||
- **Método**: `GET`
|
||||
- **Descrição**: Verifica o espaço disponível em disco
|
||||
|
||||
## Formato de Resposta
|
||||
|
||||
A resposta segue o formato padrão do Terminus:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"info": {
|
||||
"api": {
|
||||
"status": "up"
|
||||
},
|
||||
"disk": {
|
||||
"status": "up"
|
||||
},
|
||||
"memory_heap": {
|
||||
"status": "up"
|
||||
},
|
||||
"oracle": {
|
||||
"status": "up"
|
||||
},
|
||||
"postgres": {
|
||||
"status": "up"
|
||||
}
|
||||
},
|
||||
"error": {},
|
||||
"details": {
|
||||
"api": {
|
||||
"status": "up"
|
||||
},
|
||||
"disk": {
|
||||
"status": "up",
|
||||
"freeBytes": 53687091200,
|
||||
"usedBytes": 170573111296,
|
||||
"totalBytes": 224260202496
|
||||
},
|
||||
"memory_heap": {
|
||||
"status": "up",
|
||||
"usedBytes": 45731840,
|
||||
"thresholdBytes": 157286400
|
||||
},
|
||||
"oracle": {
|
||||
"status": "up"
|
||||
},
|
||||
"postgres": {
|
||||
"status": "up"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Níveis de Status
|
||||
|
||||
- **up**: O componente está funcionando corretamente
|
||||
- **down**: O componente está com problemas
|
||||
- **ok**: Todos os componentes estão funcionando corretamente
|
||||
- **error**: Um ou mais componentes estão com problemas
|
||||
|
||||
## Integração com Monitoramento
|
||||
|
||||
### Prometheus (Recomendado)
|
||||
|
||||
Para integrar com Prometheus, instale e configure o pacote `@willsoto/nestjs-prometheus`:
|
||||
|
||||
```bash
|
||||
npm install @willsoto/nestjs-prometheus prom-client --save
|
||||
```
|
||||
|
||||
E então adicione o PrometheusModule ao HealthModule:
|
||||
|
||||
```typescript
|
||||
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ...
|
||||
PrometheusModule.register(),
|
||||
],
|
||||
})
|
||||
export class HealthModule {}
|
||||
```
|
||||
|
||||
### Integração com Ferramentas de APM
|
||||
|
||||
Os health checks podem ser integrados com ferramentas de Application Performance Monitoring (APM) como:
|
||||
|
||||
- New Relic
|
||||
- Datadog
|
||||
- Dynatrace
|
||||
- Grafana
|
||||
|
||||
## Configuração de Alertas
|
||||
|
||||
Recomenda-se configurar alertas para quando os health checks falharem:
|
||||
|
||||
1. **Alertas de Banco de Dados**: Notificação imediata para problemas em conexões de banco
|
||||
2. **Alertas de Memória**: Alerta quando o uso de memória estiver próximo ao limite
|
||||
3. **Alertas de Disco**: Alerta quando o espaço em disco estiver abaixo do limite seguro
|
||||
|
||||
## Uso em Kubernetes
|
||||
|
||||
Se estiver usando Kubernetes, você pode integrar esses health checks como Readiness e Liveness Probes:
|
||||
|
||||
```yaml
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8066
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
```
|
||||
179
monitoring-implementation.md
Normal file
179
monitoring-implementation.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Implementação de Monitoramento e Alertas
|
||||
|
||||
## Visão Geral
|
||||
|
||||
Este documento descreve a implementação de um sistema completo de monitoramento e alertas para o Portal Jurunense API, baseado em Prometheus, Grafana e AlertManager. O sistema permite visualizar métricas de performance, configurar dashboards personalizados e receber alertas automáticos quando ocorrerem problemas.
|
||||
|
||||
## Componentes Implementados
|
||||
|
||||
### 1. Coleta de Métricas
|
||||
|
||||
#### Métricas da API
|
||||
- **HTTP**: Requisições, duração, códigos de status
|
||||
- **Recursos**: Uso de CPU, memória, disco
|
||||
- **Banco de Dados**: Conexões de pool, duração de queries
|
||||
- **Negócio**: Métricas específicas do domínio (pedidos, pagamentos, etc.)
|
||||
|
||||
#### Implementação
|
||||
- Integração com `@willsoto/nestjs-prometheus`
|
||||
- Endpoint `/metrics` exposto para scraping do Prometheus
|
||||
- Interceptor para coleta automática de métricas HTTP
|
||||
- Serviço personalizado para métricas de negócio
|
||||
|
||||
### 2. Dashboards Grafana
|
||||
|
||||
#### Dashboard Principal
|
||||

|
||||
|
||||
**Painéis Incluídos:**
|
||||
- Visão geral da saúde da aplicação
|
||||
- Taxa de requisições por segundo
|
||||
- Latência das requisições (percentis)
|
||||
- Uso de recursos (CPU, memória, disco)
|
||||
- Taxa de erros HTTP
|
||||
|
||||
#### Dashboard de Banco de Dados
|
||||
: Para alertas críticos fora do horário comercial
|
||||
|
||||
### 4. Arquitetura de Monitoramento
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌────────────┐ ┌─────────────┐
|
||||
│ API │ │ Prometheus │ │ Grafana │
|
||||
│ (/metrics) │───▶│ (Coleta) │───▶│ (Dashboards)│
|
||||
└─────────────┘ └────────────┘ └─────────────┘
|
||||
│ ▲
|
||||
▼ │
|
||||
┌────────────┐ ┌─────────────┐
|
||||
│AlertManager│───▶│ Notificações│
|
||||
│ (Alertas) │ │ (Email/Slack)│
|
||||
└────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## Configuração e Uso
|
||||
|
||||
### Iniciando o Sistema de Monitoramento
|
||||
|
||||
```bash
|
||||
# Iniciar todos os serviços de monitoramento
|
||||
docker-compose -f docker-compose.monitoring.yml up -d
|
||||
|
||||
# Verificar status dos serviços
|
||||
docker-compose -f docker-compose.monitoring.yml ps
|
||||
|
||||
# Visualizar logs
|
||||
docker-compose -f docker-compose.monitoring.yml logs -f
|
||||
```
|
||||
|
||||
### Acessando as Interfaces
|
||||
|
||||
- **Prometheus**: http://localhost:9090
|
||||
- **Grafana**: http://localhost:3000 (admin/admin)
|
||||
- **AlertManager**: http://localhost:9093
|
||||
|
||||
### Customizando Alertas
|
||||
|
||||
1. Edite o arquivo `monitoring/prometheus/rules/portaljuru_alerts.yml`
|
||||
2. Adicione ou modifique regras de alerta usando a sintaxe PromQL
|
||||
3. Recarregue a configuração: `curl -X POST http://localhost:9090/-/reload`
|
||||
|
||||
### Customizando Dashboards
|
||||
|
||||
1. Acesse o Grafana: http://localhost:3000
|
||||
2. Faça login com as credenciais padrão (admin/admin)
|
||||
3. Navegue até Dashboards > Browse
|
||||
4. Clone um dashboard existente ou crie um novo
|
||||
5. Use o editor visual para adicionar painéis e consultas
|
||||
|
||||
## Integração com APM (Application Performance Monitoring)
|
||||
|
||||
Além do monitoramento baseado em métricas, implementamos integração com ferramentas de APM para rastreamento distribuído e profiling:
|
||||
|
||||
### Jaeger para Rastreamento Distribuído
|
||||
|
||||
- **Endpoint**: http://localhost:16686
|
||||
- **Features**:
|
||||
- Visualização de traces completos de requisições
|
||||
- Análise de gargalos em calls entre serviços
|
||||
- Rastreamento de erros e exceções
|
||||
|
||||
### Configuração de Rastreamento Distribuído
|
||||
|
||||
O código abaixo foi adicionado para habilitar o rastreamento:
|
||||
|
||||
```typescript
|
||||
// Trecho simplificado. O código completo está no módulo de saúde.
|
||||
import { TracingModule } from './tracing.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TracingModule.forRoot({
|
||||
serviceName: 'portaljuru-api',
|
||||
samplingRate: 0.3,
|
||||
}),
|
||||
// ...
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
## Monitoramento Proativo
|
||||
|
||||
Implementamos monitoramento proativo através de:
|
||||
|
||||
1. **Health Checks Periódicos**: Verificações automáticas a cada 5 minutos
|
||||
2. **Alertas Preditivos**: Baseados em tendências anômalas de métricas
|
||||
3. **Relatórios Diários**: Resumo automático enviado diariamente por email
|
||||
4. **Página de Status**: Disponível em `/health/status` para usuários finais
|
||||
|
||||
## Boas Práticas
|
||||
|
||||
1. **Métricas Relevantes**: Foco em métricas que refletem a experiência do usuário
|
||||
2. **Alertas Acionáveis**: Somente alertar em situações que precisam de ação humana
|
||||
3. **Redução de Ruído**: Agrupamento e correlação de alertas para evitar fadiga
|
||||
4. **Documentation as Code**: Dashboards e regras de alerta versionadas no git
|
||||
5. **Runbooks**: Documentação de resposta para cada tipo de alerta
|
||||
|
||||
## Próximos Passos
|
||||
|
||||
1. **Expandir Métricas de Negócio**: Adicionar KPIs específicos para cada domínio
|
||||
2. **Machine Learning**: Implementar detecção de anomalias baseada em ML
|
||||
3. **Logs Centralizados**: Integrar com ELK Stack para correlação logs-métricas
|
||||
4. **SLOs e SLIs**: Definir e monitorar objetivos de nível de serviço
|
||||
5. **Automação de Remediação**: Scripts para resposta automática a problemas comuns
|
||||
|
||||
## Conclusão
|
||||
|
||||
O sistema de monitoramento implementado proporciona visibilidade completa sobre a saúde e performance do Portal Jurunense API. Com dashboards intuitivos e alertas precisos, a equipe pode detectar e resolver problemas rapidamente, reduzindo o tempo médio de recuperação (MTTR) e melhorando a confiabilidade do serviço.
|
||||
53
monitoring/alertmanager/config.yml
Normal file
53
monitoring/alertmanager/config.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
global:
|
||||
resolve_timeout: 5m
|
||||
# Configurar esses valores com informações reais
|
||||
smtp_smarthost: 'smtp.example.com:587'
|
||||
smtp_from: 'alertmanager@example.com'
|
||||
smtp_auth_username: 'alertmanager@example.com'
|
||||
smtp_auth_password: 'password'
|
||||
# Opcional: configuração para Slack
|
||||
slack_api_url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
|
||||
|
||||
route:
|
||||
group_by: ['alertname', 'severity']
|
||||
group_wait: 30s
|
||||
group_interval: 5m
|
||||
repeat_interval: 1h
|
||||
receiver: 'team-emails'
|
||||
routes:
|
||||
- match:
|
||||
severity: critical
|
||||
receiver: 'team-pager'
|
||||
continue: true
|
||||
- match:
|
||||
severity: warning
|
||||
receiver: 'team-emails'
|
||||
|
||||
receivers:
|
||||
- name: 'team-emails'
|
||||
email_configs:
|
||||
- to: 'team@example.com'
|
||||
send_resolved: true
|
||||
html: '{{ template "email.html" . }}'
|
||||
headers:
|
||||
Subject: '{{ template "email.subject" . }}'
|
||||
|
||||
- name: 'team-pager'
|
||||
slack_configs:
|
||||
- channel: '#alerts'
|
||||
send_resolved: true
|
||||
icon_emoji: ':warning:'
|
||||
title: '{{ template "slack.title" . }}'
|
||||
text: '{{ template "slack.text" . }}'
|
||||
# Adicione aqui outros sistemas de notificação para alertas críticos
|
||||
# Por exemplo: webhook, PagerDuty, etc.
|
||||
|
||||
templates:
|
||||
- '/etc/alertmanager/templates/*.tmpl'
|
||||
|
||||
inhibit_rules:
|
||||
- source_match:
|
||||
severity: 'critical'
|
||||
target_match:
|
||||
severity: 'warning'
|
||||
equal: ['alertname']
|
||||
11
monitoring/grafana/provisioning/dashboards/dashboard.yml
Normal file
11
monitoring/grafana/provisioning/dashboards/dashboard.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Prometheus'
|
||||
orgId: 1
|
||||
folder: ''
|
||||
type: file
|
||||
disableDeletion: false
|
||||
editable: true
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: false
|
||||
28
monitoring/prometheus/prometheus.yml
Normal file
28
monitoring/prometheus/prometheus.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
scrape_timeout: 10s
|
||||
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets:
|
||||
- alertmanager:9093
|
||||
|
||||
rule_files:
|
||||
- "rules/*.yml"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
- job_name: 'api-portaljuru'
|
||||
metrics_path: '/metrics'
|
||||
scrape_interval: 10s
|
||||
static_configs:
|
||||
- targets: ['host.docker.internal:8066']
|
||||
|
||||
- job_name: 'node-exporter'
|
||||
static_configs:
|
||||
- targets: ['node-exporter:9100']
|
||||
52
monitoring/prometheus/rules/portaljuru_alerts.yml
Normal file
52
monitoring/prometheus/rules/portaljuru_alerts.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
groups:
|
||||
- name: portaljuru_api
|
||||
rules:
|
||||
# Alerta se a API ficar inacessível por mais de 1 minuto
|
||||
- alert: PortalJuruApiDown
|
||||
expr: up{job="api-portaljuru"} == 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Portal Jurunense API fora do ar"
|
||||
description: "A API do Portal Jurunense está inacessível há pelo menos 1 minuto."
|
||||
|
||||
# Alerta se a taxa de erro HTTP for maior que 5% em 5 minutos
|
||||
- alert: HighErrorRate
|
||||
expr: sum(rate(http_request_total{job="api-portaljuru", statusCode=~"5.."}[5m])) / sum(rate(http_request_total{job="api-portaljuru"}[5m])) > 0.05
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Taxa de erro elevada na API"
|
||||
description: "A taxa de erros HTTP 5xx está acima de 5% nos últimos 5 minutos."
|
||||
|
||||
# Alerta se o tempo de resposta estiver muito alto
|
||||
- alert: SlowResponseTime
|
||||
expr: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="api-portaljuru"}[5m])) by (le)) > 1
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Tempo de resposta elevado na API"
|
||||
description: "95% das requisições estão levando mais de 1 segundo para completar."
|
||||
|
||||
# Alerta para uso alto de memória
|
||||
- alert: HighMemoryUsage
|
||||
expr: process_resident_memory_bytes{job="api-portaljuru"} > 350000000
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Uso elevado de memória"
|
||||
description: "A API está usando mais de 350MB de memória por mais de 5 minutos."
|
||||
|
||||
# Alerta para pool de conexões quase esgotado
|
||||
- alert: DatabaseConnectionPoolNearlyFull
|
||||
expr: api_db_connection_pool_used{job="api-portaljuru"} / api_db_connection_pool_total{job="api-portaljuru"} > 0.8
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Pool de conexões quase cheio"
|
||||
description: "O pool de conexões está usando mais de 80% da capacidade máxima."
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
|
||||
8924
package-lock.json
generated
8924
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
66
package.json
66
package.json
@@ -8,12 +8,11 @@
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"format": "prettier --write \"../../**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
@@ -21,54 +20,73 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/axios": "^3.1.3",
|
||||
"@nestjs/common": "^7.5.1",
|
||||
"@nestjs/core": "^7.5.1",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^7.5.1",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"@nestjs/axios": "^4.0.0",
|
||||
"@nestjs/bull": "^11.0.2",
|
||||
"@nestjs/common": "^11.0.12",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.12",
|
||||
"@nestjs/cqrs": "^11.0.3",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/microservices": "^11.0.12",
|
||||
"@nestjs/passport": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.0.12",
|
||||
"@nestjs/schematics": "^8.0.0",
|
||||
"@nestjs/swagger": "^11.1.0",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@nestjs/websockets": "^11.0.12",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/estree": "^1.0.7",
|
||||
"@willsoto/nestjs-prometheus": "^6.0.2",
|
||||
"aws-sdk": "^2.1692.0",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.8.4",
|
||||
"bullmq": "^5.46.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"fs": "0.0.1-security",
|
||||
"guid-typescript": "^1.0.9",
|
||||
"helmet": "^8.1.0",
|
||||
"https": "^1.0.0",
|
||||
"install": "^0.13.0",
|
||||
"ioredis": "^5.6.0",
|
||||
"md5": "^2.3.0",
|
||||
"md5-typescript": "^1.0.5",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"oracledb": "^5.5.0",
|
||||
"oracledb": "^6.8.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"path": "^0.12.7",
|
||||
"pg": "^8.13.3",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.8.0",
|
||||
"prom-client": "^15.1.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typeorm": "^0.3.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^7.5.1",
|
||||
"@nestjs/schematics": "^7.1.3",
|
||||
"@nestjs/testing": "^7.5.1",
|
||||
"@nestjs/cli": "^11.0.5",
|
||||
"@nestjs/schematics": "^8.0.0",
|
||||
"@nestjs/testing": "^11.0.12",
|
||||
"@types/express": "^4.17.8",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^14.14.6",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/supertest": "^2.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
||||
"@typescript-eslint/parser": "^4.6.1",
|
||||
"eslint": "^7.12.1",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"jest": "^26.6.3",
|
||||
"prettier": "^2.1.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"supertest": "^6.0.0",
|
||||
"ts-jest": "^26.4.3",
|
||||
"ts-loader": "^8.0.8",
|
||||
"ts-node": "^9.0.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"typescript": "^4.0.5"
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
||||
59
seguranca-melhorias.md
Normal file
59
seguranca-melhorias.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Melhorias de Segurança Implementadas
|
||||
|
||||
## 1. Proteção de Informações Sensíveis
|
||||
|
||||
- **Variáveis de Ambiente**: Removidas credenciais expostas do arquivo `.env` e substituídas por valores genéricos.
|
||||
- **Gitignore**: Atualizado para garantir que arquivos `.env` não sejam acidentalmente versionados.
|
||||
- **Exemplo de ENV**: Criado arquivo `.env.example` como modelo sem credenciais reais para ser versionado.
|
||||
|
||||
## 2. Proteção Contra Ataques de Força Bruta
|
||||
|
||||
- **Rate Limiting**: Implementado middleware `RateLimiterMiddleware` para limitar o número de requisições por IP e rota.
|
||||
- **Throttler Integrado**: Configurado corretamente o ThrottlerModule do NestJS para rate limiting baseado em rotas.
|
||||
- **Headers de Rate Limit**: Adicionados cabeçalhos HTTP para informar o cliente sobre limites de requisição.
|
||||
- **Configuração Flexível**: Os limites de rate limiting são configuráveis via variáveis de ambiente (`THROTTLE_TTL` e `THROTTLE_LIMIT`).
|
||||
- **Aplicação Seletiva**: Aplicado principalmente em rotas sensíveis como autenticação e usuários.
|
||||
|
||||
## 3. Validação e Sanitização de Dados
|
||||
|
||||
- **Validadores Personalizados**: Criados decorators `IsSanitized` e `IsSecureId` para validação rigorosa de entradas.
|
||||
- **Middleware de Sanitização**: Implementado `RequestSanitizerMiddleware` para limpar automaticamente todos os dados de entrada.
|
||||
- **Proteção Contra Injeções**: Adicionadas verificações contra SQL Injection, NoSQL Injection e XSS.
|
||||
- **Validação Global**: Configuração de `ValidationPipe` com opções mais rigorosas no arquivo `main.ts`.
|
||||
|
||||
## 4. Cabeçalhos HTTP Seguros
|
||||
|
||||
- **Helmet**: Adicionado o middleware Helmet para configurar cabeçalhos HTTP seguros.
|
||||
- **CORS Restritivo**: Configuração mais rigorosa para CORS, limitando origens em produção.
|
||||
- **Headers de Segurança**: Adicionados headers para proteção contra XSS, clickjacking e sniffing.
|
||||
|
||||
## 5. Recomendações Adicionais
|
||||
|
||||
- **Ambiente de Produção**: A configuração de segurança diferencia entre desenvolvimento e produção.
|
||||
- **Mensagens de Erro**: Desativação de mensagens de erro detalhadas em produção para evitar vazamento de informações.
|
||||
- **Autenticação**: Configuração de tempo de expiração do JWT mais adequada para balancear segurança e experiência.
|
||||
|
||||
## Como Usar
|
||||
|
||||
Os novos recursos de segurança são aplicados automaticamente na aplicação. Para utilizar os validadores personalizados em DTOs, importe-os assim:
|
||||
|
||||
```typescript
|
||||
import { IsSanitized, IsSecureId } from '../common/validators/sanitize.validator';
|
||||
|
||||
export class ExampleDto {
|
||||
@IsSecureId()
|
||||
id: string;
|
||||
|
||||
@IsSanitized()
|
||||
@IsString()
|
||||
content: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Próximos Passos Recomendados
|
||||
|
||||
1. Implementar auditoria de segurança regular
|
||||
2. Configurar autenticação de dois fatores
|
||||
3. Realizar análise estática de código para buscar vulnerabilidades adicionais
|
||||
4. Implementar verificação de força de senha
|
||||
5. Adicionar proteção contra CSRF para operações sensíveis
|
||||
6
src/Log/ILogger.ts
Normal file
6
src/Log/ILogger.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface ILogger {
|
||||
log(message: string): void;
|
||||
warn(message: string): void;
|
||||
error(message: string, trace?: string): void;
|
||||
}
|
||||
|
||||
26
src/Log/NestLoggerAdapter.ts
Normal file
26
src/Log/NestLoggerAdapter.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export class NestLoggerAdapter implements ILogger {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(private readonly context: string) {
|
||||
this.logger = new Logger(context);
|
||||
}
|
||||
|
||||
log(message: string, meta?: Record<string, any>): void {
|
||||
this.logger.log(this.formatMessage(message, meta));
|
||||
}
|
||||
|
||||
warn(message: string, meta?: Record<string, any>): void {
|
||||
this.logger.warn(this.formatMessage(message, meta));
|
||||
}
|
||||
|
||||
error(message: string, trace?: string, meta?: Record<string, any>): void {
|
||||
this.logger.error(this.formatMessage(message, meta), trace);
|
||||
}
|
||||
|
||||
private formatMessage(message: string, meta?: Record<string, any>): string {
|
||||
return meta ? `${message} | ${JSON.stringify(meta)}` : message;
|
||||
}
|
||||
}
|
||||
22
src/Log/logger.decorator.ts
Normal file
22
src/Log/logger.decorator.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export function LogExecution(label?: string) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
const original = descriptor.value;
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const logger: ILogger = this.logger;
|
||||
const context = label || `${target.constructor.name}.${propertyKey}`;
|
||||
const start = Date.now();
|
||||
logger.log(`Iniciando: ${context} | ${JSON.stringify({ args })}`);
|
||||
const result = await original.apply(this, args);
|
||||
const duration = Date.now() - start;
|
||||
logger.log(`Finalizado: ${context} em ${duration}ms`);
|
||||
return result;
|
||||
};
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
14
src/Log/logger.module.ts
Normal file
14
src/Log/logger.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { NestLoggerAdapter } from './NestLoggerAdapter';
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'LoggerService',
|
||||
useFactory: () => new NestLoggerAdapter('DataConsultService'),
|
||||
},
|
||||
],
|
||||
exports: ['LoggerService'],
|
||||
})
|
||||
export class LoggerModule {}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,13 @@
|
||||
import { BaseModule } from './core/services/base.module';
|
||||
import { BaseService } from './core/services/base.service';
|
||||
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { createOracleConfig } from './core/configs/typeorm.oracle.config';
|
||||
import { createPostgresConfig } from './core/configs/typeorm.postgres.config';
|
||||
import { LogisticModule } from './logistic/logistic.module';
|
||||
import { OrdersPaymentModule } from './orders-payment/orders-payment.module';
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable prettier/prettier */
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { typeOrmConfig, typeOrmPgConfig } from './core/configs/typeorm.config';
|
||||
import { ProductsModule } from './products/products.module';
|
||||
import { AuthModule } from './auth/auth/auth.module';
|
||||
import { DataConsultModule } from './data-consult/data-consult.module';
|
||||
import { OrdersModule } from './orders/orders.module';
|
||||
import { OrdersModule } from './orders/modules/orders.module';
|
||||
import { OcorrencesController } from './crm/occurrences/ocorrences.controller';
|
||||
import { OccurrencesModule } from './crm/occurrences/occurrences.module';
|
||||
import { ReasonTableModule } from './crm/reason-table/reason-table.module';
|
||||
@@ -21,26 +15,69 @@ import { NegotiationsModule } from './crm/negotiations/negotiations.module';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { LogisticController } from './logistic/logistic.controller';
|
||||
import { LogisticService } from './logistic/logistic.service';
|
||||
import { LoggerModule } from './Log/logger.module';
|
||||
import jwtConfig from './auth/jwt.config';
|
||||
import { UsersModule } from './auth/users/users.module';
|
||||
import { ProductsModule } from './products/products.module';
|
||||
import { ThrottlerModule, ThrottlerModuleOptions } from '@nestjs/throttler';
|
||||
import { RateLimiterMiddleware } from './common/middlewares/rate-limiter.middleware';
|
||||
import { RequestSanitizerMiddleware } from './common/middlewares/request-sanitizer.middleware';
|
||||
import { HealthModule } from './health/health.module';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BaseModule,
|
||||
UsersModule,
|
||||
ConfigModule.forRoot({ isGlobal: true,
|
||||
load: [jwtConfig]
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
name: 'oracle',
|
||||
inject: [ConfigService],
|
||||
useFactory: createOracleConfig,
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
name: 'postgres',
|
||||
inject: [ConfigService],
|
||||
useFactory: createPostgresConfig,
|
||||
}),
|
||||
ThrottlerModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService): ThrottlerModuleOptions => ({
|
||||
throttlers: [
|
||||
{
|
||||
ttl: config.get<number>('THROTTLE_TTL', 60),
|
||||
limit: config.get<number>('THROTTLE_LIMIT', 10),
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
LogisticModule,
|
||||
OrdersPaymentModule,
|
||||
HttpModule,
|
||||
OrdersModule,
|
||||
ProductsModule,
|
||||
NegotiationsModule,
|
||||
OccurrencesModule,
|
||||
ReasonTableModule,
|
||||
LoggerModule,
|
||||
DataConsultModule,
|
||||
ProductsModule,
|
||||
AuthModule,
|
||||
OrdersModule,
|
||||
TypeOrmModule.forRoot(typeOrmConfig),
|
||||
TypeOrmModule.forRoot(typeOrmPgConfig),
|
||||
HealthModule,
|
||||
],
|
||||
controllers: [
|
||||
OcorrencesController, AppController, LogisticController,],
|
||||
providers: [
|
||||
BaseService, AppService, LogisticService,],
|
||||
controllers: [OcorrencesController, LogisticController ],
|
||||
providers: [ LogisticService, ],
|
||||
})
|
||||
export class AppModule { }
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(RequestSanitizerMiddleware)
|
||||
.forRoutes('*');
|
||||
|
||||
consumer
|
||||
.apply(RateLimiterMiddleware)
|
||||
.forRoutes('auth', 'users');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
@@ -7,35 +5,55 @@ import {
|
||||
HttpStatus,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { UserModel } from 'src/core/models/user.model';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { AuthenticateUserCommand } from './commands/authenticate-user.command';
|
||||
import { LoginResponseDto } from './dto/LoginResponseDto';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { ResultModel } from 'src/core/models/result.model';
|
||||
import { ResetPasswordModel } from 'src/core/models/reset-password.model';
|
||||
import { ChangePasswordModel } from 'src/core/models/change-password.model';
|
||||
import { AuthService } from './auth.service';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiBody,
|
||||
ApiOkResponse,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('Auth')
|
||||
@Controller('api/v1/auth')
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private authService: AuthService,
|
||||
) { }
|
||||
private readonly commandBus: CommandBus,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() model: UserModel): Promise<any> {
|
||||
const user = await this.usersService.authenticate(model);
|
||||
if (!user)
|
||||
@ApiOperation({ summary: 'Realiza login e retorna um token JWT' })
|
||||
@ApiBody({ type: LoginDto })
|
||||
@ApiOkResponse({
|
||||
description: 'Login realizado com sucesso',
|
||||
type: LoginResponseDto,
|
||||
})
|
||||
@ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' })
|
||||
async login(@Body() dto: LoginDto): Promise<LoginResponseDto> {
|
||||
const command = new AuthenticateUserCommand(dto.username, dto.password);
|
||||
const result = await this.commandBus.execute(command);
|
||||
|
||||
if (!result.success) {
|
||||
throw new HttpException(
|
||||
new ResultModel(false, 'Usuário ou senha inválidos.', null, null),
|
||||
new ResultModel(false, result.error, null, result.error),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const user = result.data;
|
||||
const token = await this.authService.createToken(
|
||||
user.id,
|
||||
user.sellerId,
|
||||
user.username,
|
||||
user.name,
|
||||
user.email,
|
||||
user.storeId
|
||||
user.storeId,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -48,25 +66,4 @@ export class AuthController {
|
||||
token: token,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('reset-password')
|
||||
async resetPassword(@Body() resetPassword: ResetPasswordModel) {
|
||||
const response = await this.usersService.resetPassword(resetPassword);
|
||||
if (response == null) {
|
||||
throw new HttpException('Usuário não foi encontrado', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return { message: 'Senha alterada com sucesso! Foi enviado email com a nova senha!' };
|
||||
|
||||
}
|
||||
|
||||
@Post('change-password')
|
||||
async changePassword(@Body() changePassword: ChangePasswordModel) {
|
||||
const response = await this.usersService.changePassword(changePassword);
|
||||
if (response == null) {
|
||||
throw new HttpException('Usuário não foi encontrado', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return { message: 'Senha alterada com sucesso!' };
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtModule, JwtService } from '@nestjs/jwt';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from '../strategies/jwt-strategy';
|
||||
import { RedisModule } from 'src/core/configs/cache/redis.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AuthenticateUserHandler } from './commands/authenticate-user.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
PassportModule.register({
|
||||
defaultStrategy: 'jwt',
|
||||
}),
|
||||
JwtModule.register({
|
||||
secret: '4557C0D7-DFB0-40DA-BF83-91A75103F7A9',
|
||||
CqrsModule,
|
||||
ConfigModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET'),
|
||||
signOptions: {
|
||||
expiresIn: 3600,
|
||||
expiresIn: configService.get<string>('JWT_EXPIRES_IN'),
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
RedisModule,
|
||||
UsersModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { JwtPayload } from '../models/jwt-payload.model';
|
||||
import { UserRepository } from '../users/UserRepository';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@@ -11,6 +10,7 @@ export class AuthService {
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
|
||||
async createToken(id: number, sellerId: number, username: string, email: string, storeId: string) {
|
||||
@@ -25,8 +25,16 @@ export class AuthService {
|
||||
return this.jwtService.sign(user, options);
|
||||
}
|
||||
|
||||
async validateUser(payload: JwtPayload): Promise<any> {
|
||||
//return await this.accountService.findOneByUsername(payload.username);
|
||||
return payload;
|
||||
async validateUser(payload: JwtPayload): Promise<JwtPayload | null> {
|
||||
const user = await this.userRepository.findById(payload.id);
|
||||
if (!user || !user.active) return null;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
sellerId: user.sellerId,
|
||||
storeId: user.storeId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
}
|
||||
7
src/auth/auth/commands/authenticate-user.command.ts
Normal file
7
src/auth/auth/commands/authenticate-user.command.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class AuthenticateUserCommand {
|
||||
constructor(
|
||||
public readonly username: string,
|
||||
public readonly password: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
37
src/auth/auth/commands/authenticate-user.service.ts
Normal file
37
src/auth/auth/commands/authenticate-user.service.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { AuthenticateUserCommand } from './authenticate-user.command';
|
||||
import { UserRepository } from '../../users/UserRepository';
|
||||
import { Result } from '../../models/result';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UserModel } from 'src/core/models/user.model';
|
||||
|
||||
@CommandHandler(AuthenticateUserCommand)
|
||||
@Injectable()
|
||||
export class AuthenticateUserHandler implements ICommandHandler<AuthenticateUserCommand> {
|
||||
constructor(private readonly userRepository: UserRepository) {}
|
||||
|
||||
async execute(command: AuthenticateUserCommand): Promise<Result<UserModel>> {
|
||||
const { username, password } = command;
|
||||
|
||||
const user = await this.userRepository.findByUsernameAndPassword(username, password);
|
||||
|
||||
if (!user) {
|
||||
return Result.fail('Usuário ou senha inválidos');
|
||||
}
|
||||
|
||||
if (user.dataDesligamento !== null) {
|
||||
return Result.fail('Usuário desligado da empresa, login não permitido!');
|
||||
}
|
||||
|
||||
if (user.situacao === 'I') {
|
||||
return Result.fail('Usuário inativo, login não permitido!');
|
||||
}
|
||||
|
||||
if (user.situacao === 'B') {
|
||||
return Result.fail('Usuário bloqueado, login não permitido!');
|
||||
}
|
||||
|
||||
|
||||
return Result.ok(user);
|
||||
}
|
||||
}
|
||||
12
src/auth/auth/dto/LoginResponseDto.ts
Normal file
12
src/auth/auth/dto/LoginResponseDto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// login-response.dto.ts
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginResponseDto {
|
||||
@ApiProperty() id: number;
|
||||
@ApiProperty() sellerId: number;
|
||||
@ApiProperty() name: string;
|
||||
@ApiProperty() username: string;
|
||||
@ApiProperty() storeId: string;
|
||||
@ApiProperty() email: string;
|
||||
@ApiProperty() token: string;
|
||||
}
|
||||
20
src/auth/auth/dto/login.dto.ts
Normal file
20
src/auth/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({
|
||||
example: 'joelson.r',
|
||||
description: 'Usuário de login',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '1010',
|
||||
description: 'Senha do usuário',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
5
src/auth/guards/jwt-auth.guard.ts
Normal file
5
src/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
4
src/auth/jwt.config.ts
Normal file
4
src/auth/jwt.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default () => ({
|
||||
jwtSecret: process.env.JWT_SECRET || 'fallback-secret',
|
||||
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '8h',
|
||||
});
|
||||
16
src/auth/models/result.ts
Normal file
16
src/auth/models/result.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export class Result<T> {
|
||||
private constructor(
|
||||
public readonly success: boolean,
|
||||
public readonly data?: T,
|
||||
public readonly error?: string,
|
||||
) {}
|
||||
|
||||
static ok<U>(data: U): Result<U> {
|
||||
return new Result<U>(true, data);
|
||||
}
|
||||
|
||||
static fail<U>(message: string): Result<U> {
|
||||
return new Result<U>(false, undefined, message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,50 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { JwtPayload } from '../models/jwt-payload.model';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { UserRepository } from '../../auth/users/UserRepository';
|
||||
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
||||
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
constructor(
|
||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKeyProvider: '4557C0D7-DFB0-40DA-BF83-91A75103F7A9', //secretOrKey
|
||||
secretOrKey: configService.get<string>('jwtSecret'),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload) {
|
||||
const user = await this.authService.validateUser(payload);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
const sessionKey = this.buildSessionKey(payload.id);
|
||||
const cachedUser = await this.redis.get<any>(sessionKey);
|
||||
|
||||
if (cachedUser) {
|
||||
// await this.auditAccess(cachedUser);
|
||||
return cachedUser;
|
||||
}
|
||||
return user;
|
||||
|
||||
const user = await this.userRepository.findById(payload.id);
|
||||
if (!user || user.situacao === 'I' || user.dataDesligamento) {
|
||||
throw new UnauthorizedException('Usuário inválido ou inativo');
|
||||
}
|
||||
|
||||
await this.redis.set(sessionKey, user, 60 * 60 * 8); // 8h
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
|
||||
private buildSessionKey(userId: number): string {
|
||||
return `auth:sessions:${userId}`;
|
||||
}
|
||||
}
|
||||
|
||||
76
src/auth/users/UserRepository.ts
Normal file
76
src/auth/users/UserRepository.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
constructor(
|
||||
@InjectDataSource('oracle')
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async findByUsernameAndPassword(username: string, password: string) {
|
||||
const sql = `
|
||||
SELECT
|
||||
PCEMPR.MATRICULA AS "id",
|
||||
PCEMPR.NOME AS "name",
|
||||
PCEMPR.CODUSUR AS "sellerId",
|
||||
PCEMPR.CODFILIAL AS "storeId",
|
||||
PCEMPR.EMAIL AS "email",
|
||||
PCEMPR.DTDEMISSAO as "dataDesligamento",
|
||||
PCEMPR.SITUACAO as "situacao"
|
||||
FROM PCEMPR
|
||||
WHERE PCEMPR.USUARIOBD = :1
|
||||
AND PCEMPR.SENHABD = CRYPT(:2, PCEMPR.USUARIOBD)
|
||||
`;
|
||||
const users = await this.dataSource.query(sql, [
|
||||
username.toUpperCase(),
|
||||
password.toUpperCase(),
|
||||
]);
|
||||
|
||||
return users[0] || null;
|
||||
}
|
||||
|
||||
async findByCpfAndEmail(cpf: string, email: string) {
|
||||
const sql = `
|
||||
SELECT PCUSUARI.CODUSUR as "sellerId",
|
||||
PCUSUARI.NOME as "name",
|
||||
PCUSUARI.EMAIL as "email"
|
||||
FROM PCUSUARI
|
||||
WHERE REGEXP_REPLACE(PCUSUARI.CPF, '[^0-9]', '') = REGEXP_REPLACE(:1, '[^0-9]', '')
|
||||
AND PCUSUARI.EMAIL = :2
|
||||
`;
|
||||
|
||||
const users = await this.dataSource.query(sql, [cpf, email]);
|
||||
return users[0] || null;
|
||||
}
|
||||
|
||||
async updatePassword(sellerId: number, newPasswordHash: string) {
|
||||
const sql = `
|
||||
UPDATE PCUSUARI SET SENHALOGIN = :1 WHERE CODUSUR = :2
|
||||
`;
|
||||
await this.dataSource.query(sql, [newPasswordHash, sellerId]);
|
||||
}
|
||||
|
||||
async findByIdAndPassword(sellerId: number, passwordHash: string) {
|
||||
const sql = `
|
||||
SELECT CODUSUR as "sellerId", NOME as "name", EMAIL as "email"
|
||||
FROM PCUSUARI
|
||||
WHERE CODUSUR = :1 AND SENHALOGIN = :2
|
||||
`;
|
||||
const result = await this.dataSource.query(sql, [sellerId, passwordHash]);
|
||||
return result[0] || null;
|
||||
}
|
||||
|
||||
async findById(id: number) {
|
||||
const sql = `
|
||||
SELECT MATRICULA AS "id", NOME AS "name", CODUSUR AS "sellerId",
|
||||
CODFILIAL AS "storeId", EMAIL AS "email",
|
||||
DTDEMISSAO as "dataDesligamento", SITUACAO as "situacao"
|
||||
FROM PCEMPR
|
||||
WHERE MATRICULA = :1
|
||||
`;
|
||||
const result = await this.dataSource.query(sql, [id]);
|
||||
return result[0] || null;
|
||||
}
|
||||
}
|
||||
23
src/auth/users/change-password.service.ts
Normal file
23
src/auth/users/change-password.service.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { UserRepository } from '../users/UserRepository';
|
||||
import md5 = require('md5');
|
||||
|
||||
@Injectable()
|
||||
export class ChangePasswordService {
|
||||
constructor(private readonly userRepository: UserRepository) {}
|
||||
|
||||
async execute(userId: number, oldPassword: string, newPassword: string) {
|
||||
const current = await this.userRepository.findByIdAndPassword(
|
||||
userId,
|
||||
md5(oldPassword).toUpperCase(),
|
||||
);
|
||||
|
||||
if (!current) {
|
||||
throw new NotFoundException('Usuário não encontrado ou senha inválida');
|
||||
}
|
||||
|
||||
const newPasswordHash = md5(newPassword).toUpperCase();
|
||||
await this.userRepository.updatePassword(userId, newPasswordHash);
|
||||
return current;
|
||||
}
|
||||
}
|
||||
18
src/auth/users/email.service.ts
Normal file
18
src/auth/users/email.service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
async sendPasswordReset(email: string, newPassword: string) {
|
||||
const sql = `
|
||||
INSERT INTO CORRESPONDENCIAS (
|
||||
CORRESPONDENCIA_ID, DTINCLUSAO, TITULO, MENSAGEM, EMAIL, DESTINATARIO
|
||||
) VALUES (
|
||||
SEQ_CORRESPONDENCIAS.NEXTVAL, SYSDATE, 'Alteração de senha - CoteLivia',
|
||||
'Sua nova senha para acesso ao portal COTELIVIA é ${newPassword}',
|
||||
:email, :email
|
||||
)
|
||||
`;
|
||||
|
||||
console.log(`[Email enviado para ${email}] Senha: ${newPassword}`);
|
||||
}
|
||||
}
|
||||
28
src/auth/users/reset-password.service.ts
Normal file
28
src/auth/users/reset-password.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UserRepository } from './UserRepository';
|
||||
import { EmailService } from './email.service';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Guid } from 'guid-typescript';
|
||||
import * as md5 from 'md5';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class ResetPasswordService {
|
||||
constructor(
|
||||
@InjectDataSource('oracle') private readonly dataSource: DataSource,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly emailService: EmailService,
|
||||
) {}
|
||||
|
||||
async execute(document: string, email: string) {
|
||||
const user = await this.userRepository.findByCpfAndEmail(document, email);
|
||||
if (!user) return null;
|
||||
|
||||
const newPassword = Guid.create().toString().substring(0, 8);
|
||||
await this.userRepository.updatePassword(user.sellerId, md5(newPassword).toUpperCase());
|
||||
|
||||
await this.emailService.sendPasswordReset(user.email, newPassword);
|
||||
|
||||
return { ...user, newPassword };
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,25 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UsersService } from './users.service';
|
||||
import { UserRepository } from './UserRepository';
|
||||
import { AuthenticateUserHandler } from '../auth/commands/authenticate-user.service';
|
||||
import { ResetPasswordService } from './reset-password.service';
|
||||
import { ChangePasswordService } from './change-password.service';
|
||||
import { EmailService } from './email.service';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([]),
|
||||
],
|
||||
providers: [
|
||||
UsersService,
|
||||
UserRepository,
|
||||
AuthenticateUserHandler,
|
||||
ResetPasswordService,
|
||||
ChangePasswordService,
|
||||
EmailService,
|
||||
],
|
||||
exports: [UsersService,UserRepository],
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
describe('UsersService', () => {
|
||||
let service: UsersService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UsersService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UsersService>(UsersService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,152 +1,28 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import md5 = require('md5');
|
||||
import { Guid } from "guid-typescript";
|
||||
import { typeOrmConfig } from 'src/core/configs/typeorm.config';
|
||||
import { UserModel } from 'src/core/models/user.model';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthenticateUserHandler } from '../auth/commands/authenticate-user.service';
|
||||
import { ResetPasswordService } from './reset-password.service';
|
||||
import { ChangePasswordService } from './change-password.service';
|
||||
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
async authenticate(user: any): Promise<any> {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
try {
|
||||
const sql = `SELECT PCEMPR.MATRICULA AS "id"
|
||||
,PCEMPR.NOME AS "name"
|
||||
,PCEMPR.CODUSUR AS "sellerId"
|
||||
,PCEMPR.CODFILIAL AS "storeId"
|
||||
,PCEMPR.EMAIL AS "email"
|
||||
,PCEMPR.DTDEMISSAO as "dataDesligamento"
|
||||
,PCEMPR.SITUACAO as "situacao"
|
||||
FROM PCEMPR
|
||||
WHERE PCEMPR.USUARIOBD = '${user.userName}'
|
||||
AND PCEMPR.SENHABD = CRYPT('${user.password.toUpperCase()}', PCEMPR.USUARIOBD) `;
|
||||
constructor(
|
||||
private readonly authenticateUserService: AuthenticateUserHandler,
|
||||
private readonly resetPasswordService: ResetPasswordService,
|
||||
private readonly changePasswordService: ChangePasswordService,
|
||||
) {}
|
||||
|
||||
|
||||
const users = await queryRunner.manager.query(sql);
|
||||
|
||||
if (users.length == 0) {
|
||||
return null;
|
||||
async authenticate(user: { userName: string; password: string }) {
|
||||
const command = new AuthenticateUserCommand(user.userName, user.password);
|
||||
return this.authenticateUserService.execute(command);
|
||||
}
|
||||
async resetPassword(user: { document: string; email: string }) {
|
||||
return this.resetPasswordService.execute(user.document, user.email);
|
||||
}
|
||||
|
||||
const userDb = users[0];
|
||||
|
||||
if ( userDb.dataDesligamento !== null ) {
|
||||
throw new HttpException('Usuário desligado da empresa, login não permitido!', HttpStatus.FORBIDDEN);
|
||||
async changePassword(user: { id: number; password: string; newPassword: string }) {
|
||||
return this.changePasswordService.execute(user.id, user.password, user.newPassword);
|
||||
}
|
||||
|
||||
if ( userDb.situacao == 'I' ) {
|
||||
throw new HttpException('Usuário inativo, login não permitido!', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return userDb;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async resetPassword(user: any): Promise<any> {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
let sql =
|
||||
'SELECT PCUSUARI.CODUSUR as "sellerId" ' +
|
||||
' ,PCUSUARI.NOME as "name" ' +
|
||||
' ,PCUSUARI.EMAIL as "email" ' +
|
||||
' FROM PCUSUARI ' +
|
||||
` WHERE REGEXP_REPLACE(PCUSUARI.CPF, '[^0-9]', '') = REGEXP_REPLACE(:1, '[^0-9]', '') ` +
|
||||
` AND PCUSUARI.EMAIL = :2 `;
|
||||
|
||||
const users = await queryRunner.manager.query(sql, [
|
||||
user.document,
|
||||
user.email,
|
||||
]);
|
||||
|
||||
if (users.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const guid = Guid.create();
|
||||
console.log(guid.toString());
|
||||
const password = guid.toString().substring(0, 8);
|
||||
const newPassword = md5(password).toUpperCase();
|
||||
console.log("Senha:" + newPassword)
|
||||
sql = `UPDATE PCUSUARI SET ` +
|
||||
` SENHALOGIN = :1 ` +
|
||||
`WHERE CODUSUR = :2`;
|
||||
await queryRunner.manager.query(sql, [newPassword, users[0].sellerId]);
|
||||
|
||||
const sqlEmail = `INSERT INTO CORRESPONDENCIAS ( CORRESPONDENCIA_ID, DTINCLUSAO, TITULO, MENSAGEM, EMAIL, DESTINATARIO )
|
||||
VALUES ( SEQ_CORRESPONDENCIAS.NEXTVAL, SYSDATE, 'Alteração de email - CoteLivia',
|
||||
'Sua senha para acesso ao portal COTELIVIA é ${password}', '${users[0].email}', '${users[0].email}' )`;
|
||||
await queryRunner.manager.query(sqlEmail);
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
const userDb = users[0];
|
||||
return userDb;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
console.log(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async changePassword(user: any): Promise<any> {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
console.log(JSON.stringify(user));
|
||||
try {
|
||||
let sql =
|
||||
'SELECT PCUSUARI.CODUSUR as "sellerId" ' +
|
||||
' ,PCUSUARI.NOME as "name" ' +
|
||||
' ,PCUSUARI.EMAIL as "email" ' +
|
||||
' FROM PCUSUARI ' +
|
||||
` WHERE PCUSUARI.CODUSUR = :1` +
|
||||
` AND PCUSUARI.SENHALOGIN = :2 `;
|
||||
|
||||
const users = await queryRunner.manager.query(sql, [
|
||||
user.id,
|
||||
md5(user.password).toUpperCase(),
|
||||
]);
|
||||
|
||||
if (users.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
sql = `UPDATE PCUSUARI SET ` +
|
||||
` SENHALOGIN = :1 ` +
|
||||
`WHERE CODUSUR = :2`;
|
||||
await queryRunner.manager.query(sql, [md5(user.newPassword).toUpperCase(), users[0].sellerId]);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
const userDb = users[0];
|
||||
return userDb;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
console.log(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
64
src/common/middlewares/rate-limiter.middleware.ts
Normal file
64
src/common/middlewares/rate-limiter.middleware.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { ThrottlerException } from '@nestjs/throttler';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class RateLimiterMiddleware implements NestMiddleware {
|
||||
private readonly ttl: number;
|
||||
private readonly limit: number;
|
||||
private readonly store: Map<string, { count: number; expiration: number }> = new Map();
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.ttl = this.configService.get<number>('THROTTLE_TTL', 60);
|
||||
this.limit = this.configService.get<number>('THROTTLE_LIMIT', 10);
|
||||
}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction): void {
|
||||
// Skip if the request method is OPTIONS (for CORS preflight)
|
||||
if (req.method === 'OPTIONS') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const key = this.generateKey(req);
|
||||
const now = Date.now();
|
||||
|
||||
if (!this.store.has(key)) {
|
||||
this.store.set(key, { count: 1, expiration: now + this.ttl * 1000 });
|
||||
this.setRateLimitHeaders(res, 1);
|
||||
return next();
|
||||
}
|
||||
|
||||
const record = this.store.get(key);
|
||||
|
||||
if (record.expiration < now) {
|
||||
record.count = 1;
|
||||
record.expiration = now + this.ttl * 1000;
|
||||
this.setRateLimitHeaders(res, 1);
|
||||
return next();
|
||||
}
|
||||
|
||||
if (record.count >= this.limit) {
|
||||
const timeToWait = Math.ceil((record.expiration - now) / 1000);
|
||||
this.setRateLimitHeaders(res, record.count);
|
||||
res.header('Retry-After', String(timeToWait));
|
||||
throw new ThrottlerException(`Too Many Requests. Retry after ${timeToWait} seconds.`);
|
||||
}
|
||||
|
||||
record.count++;
|
||||
this.setRateLimitHeaders(res, record.count);
|
||||
return next();
|
||||
}
|
||||
|
||||
private generateKey(req: Request): string {
|
||||
// Combina IP com rota para rate limiting mais preciso
|
||||
const ip = req.ip || req.headers['x-forwarded-for'] as string || 'unknown-ip';
|
||||
const path = req.path || req.originalUrl || '';
|
||||
return `${ip}:${path}`;
|
||||
}
|
||||
|
||||
private setRateLimitHeaders(res: Response, count: number): void {
|
||||
res.header('X-RateLimit-Limit', String(this.limit));
|
||||
res.header('X-RateLimit-Remaining', String(Math.max(0, this.limit - count)));
|
||||
}
|
||||
}
|
||||
48
src/common/middlewares/request-sanitizer.middleware.ts
Normal file
48
src/common/middlewares/request-sanitizer.middleware.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class RequestSanitizerMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
if (req.headers) {
|
||||
this.sanitizeObject(req.headers);
|
||||
}
|
||||
|
||||
if (req.query) {
|
||||
this.sanitizeObject(req.query);
|
||||
}
|
||||
|
||||
if (req.body) {
|
||||
this.sanitizeObject(req.body);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
private sanitizeObject(obj: any) {
|
||||
Object.keys(obj).forEach(key => {
|
||||
if (typeof obj[key] === 'string') {
|
||||
obj[key] = this.sanitizeString(obj[key]);
|
||||
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
this.sanitizeObject(obj[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private sanitizeString(str: string): string {
|
||||
// Remover tags HTML básicas
|
||||
str = str.replace(/<(|\/|[^>\/bi]|\/[^>bi]|[^\/>][^>]+|\/[^>][^>]+)>/g, '');
|
||||
|
||||
// Remover scripts JavaScript
|
||||
str = str.replace(/javascript:/g, '');
|
||||
str = str.replace(/on\w+=/g, '');
|
||||
|
||||
// Remover comentários HTML
|
||||
str = str.replace(/<!--[\s\S]*?-->/g, '');
|
||||
|
||||
// Sanitizar caracteres especiais para evitar SQL injection
|
||||
str = str.replace(/'/g, "''");
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
21
src/common/response.interceptor.ts
Normal file
21
src/common/response.interceptor.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ResultModel } from '../shared/ResultModel';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor<T> implements NestInterceptor<T, ResultModel<T>> {
|
||||
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<ResultModel<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
return ResultModel.success(data);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
68
src/common/validators/sanitize.validator.ts
Normal file
68
src/common/validators/sanitize.validator.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';
|
||||
|
||||
// Decorator para sanitizar strings e prevenir SQL/NoSQL injection
|
||||
export function IsSanitized(validationOptions?: ValidationOptions) {
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isSanitized',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
if (typeof value !== 'string') return true; // Skip non-string values
|
||||
|
||||
const sqlInjectionRegex = /('|"|;|--|\/\*|\*\/|@@|@|char|nchar|varchar|nvarchar|alter|begin|cast|create|cursor|declare|delete|drop|end|exec|execute|fetch|insert|kill|open|select|sys|sysobjects|syscolumns|table|update|xp_)/i;
|
||||
if (sqlInjectionRegex.test(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for NoSQL injection patterns (MongoDB)
|
||||
const noSqlInjectionRegex = /(\$where|\$ne|\$gt|\$lt|\$gte|\$lte|\$in|\$nin|\$or|\$and|\$regex|\$options|\$elemMatch|\{.*\:.*\})/i;
|
||||
if (noSqlInjectionRegex.test(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for XSS attempts
|
||||
const xssRegex = /(<script|javascript:|on\w+\s*=|<%=|<img|<iframe|alert\(|window\.|document\.)/i;
|
||||
if (xssRegex.test(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return 'A entrada contém caracteres inválidos ou padrões potencialmente maliciosos';
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Decorator para validar IDs seguros (evita injeção em IDs)
|
||||
export function IsSecureId(validationOptions?: ValidationOptions) {
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isSecureId',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
if (typeof value !== 'string' && typeof value !== 'number') return false;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// Permitir apenas: letras, números, hífens, underscores e GUIDs
|
||||
return /^[a-zA-Z0-9\-_]+$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
||||
}
|
||||
|
||||
// Se for número, deve ser positivo
|
||||
return value > 0;
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return 'O ID fornecido não é seguro ou está em formato inválido';
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
6
src/core/configs/cache/IRedisClient.ts
vendored
Normal file
6
src/core/configs/cache/IRedisClient.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface IRedisClient {
|
||||
get<T>(key: string): Promise<T | null>;
|
||||
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
||||
del(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
145
src/core/configs/cache/index.html
vendored
Normal file
145
src/core/configs/cache/index.html
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Documentação - Integração Redis</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #007acc;
|
||||
}
|
||||
code, pre {
|
||||
background-color: #eee;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
th {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
}
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background: #007acc;
|
||||
color: white;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>📦 Integração Redis com Abstração - Portal Juru API</h1>
|
||||
|
||||
<h2>🧱 Arquitetura</h2>
|
||||
<p>O projeto utiliza o Redis com uma interface genérica para garantir desacoplamento, facilidade de teste e reaproveitamento em múltiplos módulos.</p>
|
||||
|
||||
<h3>🔌 Interface IRedisClient</h3>
|
||||
<pre><code>export interface IRedisClient {
|
||||
get<T>(key: string): Promise<T | null>;
|
||||
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
||||
del(key: string): Promise<void>;
|
||||
}</code></pre>
|
||||
|
||||
<h3>🧩 Provider REDIS_CLIENT</h3>
|
||||
<p>Faz a conexão direta com o Redis usando a biblioteca <code>ioredis</code> e o <code>ConfigService</code> para pegar host e porta.</p>
|
||||
<pre><code>export const RedisProvider: Provider = {
|
||||
provide: 'REDIS_CLIENT',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const redis = new Redis({
|
||||
host: configService.get('REDIS_HOST', '10.1.1.109'),
|
||||
port: configService.get('REDIS_PORT', 6379),
|
||||
});
|
||||
|
||||
redis.on('error', (err) => {
|
||||
console.error('Erro ao conectar ao Redis:', err);
|
||||
});
|
||||
|
||||
return redis;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
};</code></pre>
|
||||
|
||||
<h3>📦 RedisClientAdapter (Wrapper)</h3>
|
||||
<p>Classe que implementa <code>IRedisClient</code> e encapsula as operações de cache. É injetada em serviços via token.</p>
|
||||
<pre><code>@Injectable()
|
||||
export class RedisClientAdapter implements IRedisClient {
|
||||
constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const data = await this.redis.get(key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttlSeconds = 300): Promise<void> {
|
||||
await this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
await this.redis.del(key);
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h3>🔗 Token e Provider</h3>
|
||||
<p>Token de injeção definido para o adapter:</p>
|
||||
<pre><code>export const RedisClientToken = 'RedisClientInterface';
|
||||
|
||||
export const RedisClientAdapterProvider = {
|
||||
provide: RedisClientToken,
|
||||
useClass: RedisClientAdapter,
|
||||
};</code></pre>
|
||||
|
||||
<h3>📦 Módulo Global RedisModule</h3>
|
||||
<p>Torna o Redis disponível em toda a aplicação.</p>
|
||||
<pre><code>@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [RedisProvider, RedisClientAdapterProvider],
|
||||
exports: [RedisProvider, RedisClientAdapterProvider],
|
||||
})
|
||||
export class RedisModule {}</code></pre>
|
||||
|
||||
<h2>🧠 Uso em Serviços</h2>
|
||||
<p>Injetando o cache no seu service:</p>
|
||||
<pre><code>constructor(
|
||||
@Inject(RedisClientToken)
|
||||
private readonly redisClient: IRedisClient
|
||||
) {}</code></pre>
|
||||
|
||||
<p>Uso típico:</p>
|
||||
<pre><code>const data = await this.redisClient.get<T>('chave');
|
||||
if (!data) {
|
||||
const result = await fetchFromDb();
|
||||
await this.redisClient.set('chave', result, 3600);
|
||||
}</code></pre>
|
||||
|
||||
<h2>🧰 Boas práticas</h2>
|
||||
<ul>
|
||||
<li>✅ TTL por recurso (ex: produtos: 1h, lojas: 24h)</li>
|
||||
<li>✅ Nomear chaves com prefixos por domínio (ex: <code>data-consult:sellers</code>)</li>
|
||||
<li>✅ Centralizar helpers como <code>getOrSetCache</code> para evitar repetição</li>
|
||||
<li>✅ Usar <code>JSON.stringify</code> e <code>JSON.parse</code> no adapter</li>
|
||||
<li>✅ Marcar módulo como <code>@Global()</code> para acesso em toda a aplicação</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Última atualização:</strong> 29/03/2025</p>
|
||||
</body>
|
||||
</html>
|
||||
8
src/core/configs/cache/redis-client.adapter.provider.ts
vendored
Normal file
8
src/core/configs/cache/redis-client.adapter.provider.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
import { RedisClientAdapter } from './redis-client.adapter';
|
||||
export const RedisClientToken = 'RedisClientInterface';
|
||||
|
||||
export const RedisClientAdapterProvider = {
|
||||
provide: RedisClientToken,
|
||||
useClass: RedisClientAdapter,
|
||||
};
|
||||
24
src/core/configs/cache/redis-client.adapter.ts
vendored
Normal file
24
src/core/configs/cache/redis-client.adapter.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Redis } from 'ioredis';
|
||||
import { IRedisClient } from './IRedisClient';
|
||||
|
||||
@Injectable()
|
||||
export class RedisClientAdapter implements IRedisClient {
|
||||
constructor(
|
||||
@Inject('REDIS_CLIENT')
|
||||
private readonly redis: Redis
|
||||
) {}
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const data = await this.redis.get(key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttlSeconds = 300): Promise<void> {
|
||||
await this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
await this.redis.del(key);
|
||||
}
|
||||
}
|
||||
12
src/core/configs/cache/redis.module.ts
vendored
Normal file
12
src/core/configs/cache/redis.module.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { RedisProvider } from './redis.provider';
|
||||
import { RedisClientAdapterProvider } from './redis-client.adapter.provider';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [RedisProvider, RedisClientAdapterProvider],
|
||||
exports: [RedisProvider, RedisClientAdapterProvider],
|
||||
})
|
||||
export class RedisModule {}
|
||||
22
src/core/configs/cache/redis.provider.ts
vendored
Normal file
22
src/core/configs/cache/redis.provider.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export const RedisProvider: Provider = {
|
||||
provide: 'REDIS_CLIENT',
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const redis = new Redis({
|
||||
host: configService.get<string>('REDIS_HOST', 'redis-17317.crce181.sa-east-1-2.ec2.redns.redis-cloud.com'),
|
||||
port: configService.get<number>('REDIS_PORT', 17317),
|
||||
username: configService.get<string>('REDIS_USERNAME', 'default' ),
|
||||
password: configService.get<string>('REDIS_PASSWORD', 'd8sVttpJdNxrWjYRK43QGAKzEt3I8HVc'),
|
||||
});
|
||||
|
||||
redis.on('error', (err) => {
|
||||
console.error('Erro ao conectar ao Redis:', err);
|
||||
});
|
||||
|
||||
return redis;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
};
|
||||
@@ -1,22 +1,16 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
import { DataSourceOptions } from 'typeorm/data-source';
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export const typeOrmConfig: DataSourceOptions = {
|
||||
type: 'oracle',
|
||||
connectString: '(DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = 10.1.1.241)(PORT = 1521)))(CONNECT_DATA = (SERVICE_NAME = WINT)))',
|
||||
username: 'SEVEN',
|
||||
password: 'USR54SEV',
|
||||
synchronize: false,
|
||||
logging: false,
|
||||
entities: [__dirname + '/../**/*.entity.{js,ts}'],
|
||||
};
|
||||
|
||||
export const typeOrmPgConfig: DataSourceOptions = {
|
||||
type: 'postgres',
|
||||
host: '10.1.1.222',
|
||||
port: 5432,
|
||||
username: 'ti',
|
||||
password: 'ti',
|
||||
database: 'ksdb',
|
||||
synchronize: true,
|
||||
}
|
||||
export const databaseConfig = registerAs('database', () => ({
|
||||
oracle: {
|
||||
connectString: `(DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = ${process.env.ORACLE_HOST})(PORT = ${process.env.ORACLE_PORT})))(CONNECT_DATA = (SERVICE_NAME = ${process.env.ORACLE_SERVICE})))`,
|
||||
username: process.env.ORACLE_USER,
|
||||
password: process.env.ORACLE_PASSWORD,
|
||||
},
|
||||
postgres: {
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432', 10),
|
||||
username: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: process.env.POSTGRES_DB,
|
||||
},
|
||||
}));
|
||||
|
||||
59
src/core/configs/typeorm.oracle.config.ts
Normal file
59
src/core/configs/typeorm.oracle.config.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { DataSourceOptions } from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as oracledb from 'oracledb';
|
||||
|
||||
|
||||
|
||||
// Inicializar o cliente Oracle
|
||||
oracledb.initOracleClient({ libDir: process.env.ORACLE_CLIENT_LIB_DIR });
|
||||
|
||||
// Definir a estratégia de pool padrão para Oracle
|
||||
oracledb.poolTimeout = 60; // timeout do pool em segundos
|
||||
oracledb.queueTimeout = 60000; // timeout da fila em milissegundos
|
||||
oracledb.poolIncrement = 1; // incremental de conexões
|
||||
|
||||
export function createOracleConfig(config: ConfigService): DataSourceOptions {
|
||||
|
||||
// Obter configurações de ambiente ou usar valores padrão
|
||||
const poolMin = parseInt(config.get('ORACLE_POOL_MIN', '5'));
|
||||
const poolMax = parseInt(config.get('ORACLE_POOL_MAX', '20'));
|
||||
const poolIncrement = parseInt(config.get('ORACLE_POOL_INCREMENT', '5'));
|
||||
const poolTimeout = parseInt(config.get('ORACLE_POOL_TIMEOUT', '30000'));
|
||||
const idleTimeout = parseInt(config.get('ORACLE_POOL_IDLE_TIMEOUT', '300000'));
|
||||
|
||||
// Validação de valores mínimos
|
||||
const validPoolMin = Math.max(1, poolMin);
|
||||
const validPoolMax = Math.max(validPoolMin + 1, poolMax);
|
||||
const validPoolIncrement = Math.max(1, poolIncrement);
|
||||
|
||||
// Certifique-se de que poolMax é maior que poolMin
|
||||
if (validPoolMax <= validPoolMin) {
|
||||
console.warn('Warning: poolMax deve ser maior que poolMin. Ajustando poolMax para poolMin + 1');
|
||||
}
|
||||
|
||||
const options: DataSourceOptions = {
|
||||
type: 'oracle',
|
||||
connectString: config.get('ORACLE_CONNECT_STRING'),
|
||||
username: config.get('ORACLE_USER'),
|
||||
password: config.get('ORACLE_PASSWORD'),
|
||||
synchronize: false,
|
||||
logging: config.get('NODE_ENV') === 'development',
|
||||
entities: [__dirname + '/../**/*.entity.{ts,js}'],
|
||||
extra: {
|
||||
// Configurações de pool
|
||||
poolMin: validPoolMin,
|
||||
poolMax: validPoolMax,
|
||||
poolIncrement: validPoolIncrement,
|
||||
poolTimeout: Math.floor(poolTimeout / 1000), // convertido para segundos (oracledb usa segundos)
|
||||
queueTimeout: 60000, // tempo máximo para esperar na fila
|
||||
enableStats: true, // habilita estatísticas do pool
|
||||
homogeneous: true, // todas as conexões usam o mesmo usuário
|
||||
poolPingInterval: 60, // intervalo de ping em segundos
|
||||
stmtCacheSize: 30, // tamanho do cache de statements
|
||||
connectionClass: 'PORTALJURU', // classe de conexão para identificação
|
||||
idleTimeout: Math.floor(idleTimeout / 1000), // tempo de idle em segundos
|
||||
},
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
47
src/core/configs/typeorm.postgres.config.ts
Normal file
47
src/core/configs/typeorm.postgres.config.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { DataSourceOptions } from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export function createPostgresConfig(config: ConfigService): DataSourceOptions {
|
||||
// Obter configurações de ambiente ou usar valores padrão
|
||||
const poolMin = parseInt(config.get('POSTGRES_POOL_MIN', '5'));
|
||||
const poolMax = parseInt(config.get('POSTGRES_POOL_MAX', '20'));
|
||||
const idleTimeout = parseInt(config.get('POSTGRES_POOL_IDLE_TIMEOUT', '30000'));
|
||||
const connectionTimeout = parseInt(config.get('POSTGRES_POOL_CONNECTION_TIMEOUT', '5000'));
|
||||
const acquireTimeout = parseInt(config.get('POSTGRES_POOL_ACQUIRE_TIMEOUT', '60000'));
|
||||
|
||||
// Validação de valores mínimos
|
||||
const validPoolMin = Math.max(1, poolMin);
|
||||
const validPoolMax = Math.max(validPoolMin + 1, poolMax);
|
||||
const validIdleTimeout = Math.max(1000, idleTimeout);
|
||||
const validConnectionTimeout = Math.max(1000, connectionTimeout);
|
||||
const validAcquireTimeout = Math.max(1000, acquireTimeout);
|
||||
|
||||
const options: DataSourceOptions = {
|
||||
type: 'postgres',
|
||||
host: config.get('POSTGRES_HOST'),
|
||||
port: parseInt(config.get('POSTGRES_PORT', '5432')),
|
||||
username: config.get('POSTGRES_USER'),
|
||||
password: config.get('POSTGRES_PASSWORD'),
|
||||
database: config.get('POSTGRES_DB'),
|
||||
synchronize: config.get('NODE_ENV') === 'development',
|
||||
entities: [__dirname + '/../**/*.entity.{ts,js}'],
|
||||
ssl: config.get('NODE_ENV') === 'production' ? { rejectUnauthorized: false } : false,
|
||||
logging: config.get('NODE_ENV') === 'development',
|
||||
poolSize: validPoolMax, // máximo de conexões no pool
|
||||
extra: {
|
||||
// Configuração de pool do PostgreSQL
|
||||
min: validPoolMin, // mínimo de conexões no pool
|
||||
max: validPoolMax, // máximo de conexões no pool
|
||||
idleTimeoutMillis: validIdleTimeout, // tempo máximo de inatividade antes de fechar
|
||||
connectionTimeoutMillis: validConnectionTimeout, // tempo máximo para conectar
|
||||
acquireTimeoutMillis: validAcquireTimeout, // tempo máximo para adquirir uma conexão
|
||||
statement_timeout: 10000, // tempo máximo para executar uma query (10 segundos)
|
||||
query_timeout: 10000, // tempo máximo para executar uma query (10 segundos)
|
||||
},
|
||||
cache: {
|
||||
duration: 60000, // cache de consultas por 1 minuto
|
||||
},
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
1
src/core/constants.ts
Normal file
1
src/core/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const DATA_SOURCE = 'DATA_SOURCE';
|
||||
23
src/core/database/database.module.ts
Normal file
23
src/core/database/database.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { DATA_SOURCE } from '../constants';
|
||||
import { createOracleConfig } from '../configs/typeorm.oracle.config';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: DATA_SOURCE,
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
const dataSource = new DataSource(createOracleConfig(configService));
|
||||
await dataSource.initialize();
|
||||
return dataSource;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [DATA_SOURCE],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
@@ -5,9 +5,9 @@ export class Invoice {
|
||||
reasonId: number;
|
||||
reasonText: string;
|
||||
status: string;
|
||||
}
|
||||
}
|
||||
|
||||
export class CarInDelivery {
|
||||
export class CarInDelivery {
|
||||
finalKm: number;
|
||||
invoices: Invoice[];
|
||||
licensePlate: string;
|
||||
@@ -21,5 +21,4 @@ export class Invoice {
|
||||
remnant: string;
|
||||
observationRemnant: string;
|
||||
imagesRemnant: string[];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,8 @@ export class CarOutDelivery {
|
||||
vehicleCode: number; // Código do veículo
|
||||
}
|
||||
|
||||
|
||||
export class Helper {
|
||||
id: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { DropAction } from './../../../node_modules/aws-sdk/clients/mailmanager.d';
|
||||
/*
|
||||
https://docs.nestjs.com/controllers#controllers
|
||||
*/
|
||||
|
||||
import {
|
||||
Body, Controller, Get, HttpException, HttpStatus, Post, Query, Req, UseInterceptors,
|
||||
UploadedFile
|
||||
} from '@nestjs/common';
|
||||
import { BaseService } from './base.service';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { diskStorage } from 'multer';
|
||||
import { extname } from 'path';
|
||||
import * as fs from "fs";
|
||||
|
||||
@Controller('api/v1/base')
|
||||
export class BaseController {
|
||||
constructor(public readonly baseService: BaseService) { }
|
||||
// @UseGuards(JwtAuthGuard)
|
||||
@Get('execute-view')
|
||||
/* @ApiOperation({
|
||||
summary: 'Executa uma view com ou sem parâmetros',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Dados retornados com sucesso.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'O nome da view é obrigatório.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 500,
|
||||
description: 'Erro ao executar a view.',
|
||||
})*/
|
||||
async executeView(
|
||||
@Query('viewName') viewName: string,
|
||||
@Query() params: Record<string, any>,
|
||||
) {
|
||||
if (!viewName) {
|
||||
throw new HttpException(
|
||||
'O nome da view é obrigatório.',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
try {
|
||||
return await this.baseService.executeView(viewName, params);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
`Erro ao executar a view: ${error.message}`,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('send-image')
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: diskStorage({
|
||||
// Pasta onde os arquivos serão salvos; certifique-se que essa pasta exista ou crie-a automaticamente
|
||||
destination: './uploads',
|
||||
filename: (req, file, callback) => {
|
||||
// Gera um nome único para o arquivo
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const fileExtName = extname(file.originalname);
|
||||
callback(null, `${file.fieldname}-${uniqueSuffix}${fileExtName}`);
|
||||
},
|
||||
}),
|
||||
// Opcional: definir limites (ex.: tamanho máximo do arquivo)
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
|
||||
}),
|
||||
)
|
||||
async sendImage(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Body('licensePlate') licensePlate: string,
|
||||
) {
|
||||
if (!file) {
|
||||
throw new HttpException('Nenhum arquivo enviado', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Aqui você pode processar o arquivo (ex.: enviar para o S3) ou armazená-lo no disco mesmo.
|
||||
// Neste exemplo, retornamos a URL do arquivo salvo localmente.
|
||||
this.baseService.sendImages('./uploads/'+file.filename);
|
||||
fs.unlink('./uploads/'+file.filename, () => {});
|
||||
return {
|
||||
success: true,
|
||||
message: 'Upload realizado com sucesso',
|
||||
url: `https://jur-saidaretornoveiculo.s3.sa-east-1.amazonaws.com/${file.filename}`,
|
||||
licensePlate,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { BaseController } from './base.controller';
|
||||
/*
|
||||
https://docs.nestjs.com/modules
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BaseService } from './base.service';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [
|
||||
BaseController,],
|
||||
providers: [BaseService,],
|
||||
})
|
||||
export class BaseModule { }
|
||||
@@ -1,289 +0,0 @@
|
||||
import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { typeOrmConfig } from '../configs/typeorm.config';
|
||||
import { S3 } from 'aws-sdk';
|
||||
import * as fs from "fs";
|
||||
|
||||
@Injectable()
|
||||
export class BaseService {
|
||||
constructor() { }
|
||||
|
||||
async findAll(table: string) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
const [rows] = await queryRunner.query(`SELECT * FROM ${table}`);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
this.handleDatabaseError(error, `Erro ao buscar todos os registros da tabela ${table}`);
|
||||
} finally {
|
||||
queryRunner.release();
|
||||
dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async findOne(table: string, id: any) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
const [rows] = await queryRunner.query(`SELECT * FROM ${table} WHERE id = '${id}'`);
|
||||
return rows[0];
|
||||
} catch (error) {
|
||||
this.handleDatabaseError(error, `Erro ao buscar o registro com ID ${id} na tabela ${table}`);
|
||||
} finally {
|
||||
queryRunner.release();
|
||||
dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async create(table: string, data: any) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
const columns = Object.keys(data).map((key) => `${key}`).join(', ');
|
||||
const values = Object.values(data);
|
||||
const placeholders = values.map(() => '?').join(', ');
|
||||
|
||||
const query = `INSERT INTO ${table} (${columns}) VALUES (${placeholders})`;
|
||||
|
||||
const [result] = await queryRunner.query(query, values);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.handleDatabaseError(error, `Erro ao criar um registro na tabela ${table}`);
|
||||
} finally {
|
||||
queryRunner.release();
|
||||
dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async update(table: string, where: any, data: any) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
const [result] = await queryRunner.query(`UPDATE ${table} SET ${data} WHERE ${where}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.handleDatabaseError(error, `Erro ao atualizar o registro com ${where} na tabela ${table}`);
|
||||
} finally {
|
||||
queryRunner.release();
|
||||
dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async delete(table: string, where: any) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
const [result] = await queryRunner.query(`DELETE FROM ${table} WHERE ${where}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.handleDatabaseError(error, `Erro ao deletar o registro com ID ${where} na tabela ${table}`);
|
||||
} finally {
|
||||
queryRunner.release();
|
||||
dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async query(queryString: string, params: any[]): Promise<any[]> {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
const [rows] = await queryRunner.query(queryString, params);
|
||||
return rows as any[];
|
||||
} catch (error) {
|
||||
this.handleDatabaseError(error, `Erro ao executar a consulta SQL personalizada`);
|
||||
} finally {
|
||||
queryRunner.release();
|
||||
dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async executeView(viewName: string, params: Record<string, any>) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
// Valida se o nome da view foi fornecido
|
||||
if (!viewName) {
|
||||
throw new Error('O nome da view é obrigatório.');
|
||||
}
|
||||
|
||||
console.log(`Iniciando execução da view: ${viewName}`);
|
||||
console.log('Parâmetros recebidos:', params);
|
||||
|
||||
const conditions: string[] = [];
|
||||
const values: any[] = [];
|
||||
|
||||
// Remove o parâmetro viewName dos parâmetros antes de processar
|
||||
const filteredParams = { ...params };
|
||||
delete filteredParams.viewName;
|
||||
|
||||
// Adiciona as condições baseadas nos parâmetros fornecidos
|
||||
if (filteredParams && Object.keys(filteredParams).length > 0) {
|
||||
console.log('Adicionando condições para os parâmetros fornecidos...');
|
||||
for (const [key, value] of Object.entries(filteredParams)) {
|
||||
// Verifica se a chave e o valor são válidos
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
console.log(`Parâmetro válido: ${key} = '${value}'`);
|
||||
conditions.push(`${key} = '${value}'`); // Adiciona aspas para evitar problemas de SQL injection
|
||||
values.push(value);
|
||||
} else {
|
||||
console.warn(`Parâmetro ignorado: ${key} = '${value}'`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('Nenhum parâmetro válido foi fornecido.');
|
||||
}
|
||||
|
||||
// Monta a cláusula WHERE somente se houver condições
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const query = `SELECT * FROM ${ viewName } ${whereClause}`;
|
||||
|
||||
console.log(`Consulta SQL montada: ${ query }`);
|
||||
console.log(`{Valores para a consulta:, ${values}`);
|
||||
|
||||
// Executa a consulta
|
||||
const rows = await queryRunner.query(query);
|
||||
|
||||
console.log(`Consulta executada com sucesso.Linhas retornadas: ${ JSON.stringify(rows) }`);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error(`Erro ao executar a view ${ viewName }: `, error.message);
|
||||
this.handleDatabaseError(
|
||||
error,
|
||||
`Erro ao executar a view ${ viewName } com parâmetros.`,
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async executeProcedure(procedureName: string, params: Record<string, any>) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
const placeholders = Object.keys(params)
|
||||
.map(() => '?')
|
||||
.join(', ');
|
||||
const values = Object.values(params);
|
||||
|
||||
const query = `EXECUTE IMMEDIATE ${ procedureName }(${ placeholders })`;
|
||||
|
||||
// Log da query e dos valores
|
||||
console.log('Query executada:', query);
|
||||
console.log('Valores:', values);
|
||||
|
||||
const [result] = await queryRunner.query(query, values);
|
||||
|
||||
// Verifica e converte campos que contenham JSON strings para objetos
|
||||
const parsedResult = Array.isArray(result)
|
||||
? result.map((row) => {
|
||||
const parsedRow = { ...row };
|
||||
for (const [key, value] of Object.entries(parsedRow)) {
|
||||
try {
|
||||
// Tenta converter strings JSON para objetos
|
||||
if (typeof value === 'string' && value.trim().startsWith('{') && value.trim().endsWith('}')) {
|
||||
parsedRow[key] = JSON.parse(value);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignora se a conversão falhar
|
||||
console.warn(`Campo ${ key } não é um JSON válido.Mantendo como string.`);
|
||||
}
|
||||
}
|
||||
return parsedRow;
|
||||
})
|
||||
: result;
|
||||
|
||||
// Retorna os valores e o resultado
|
||||
return {
|
||||
message: 'Procedure executada com sucesso.',
|
||||
executedQuery: query,
|
||||
values: values,
|
||||
result: parsedResult,
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleDatabaseError(
|
||||
error,
|
||||
`Erro ao executar a procedure ${ procedureName } com parâmetros.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleDatabaseError(error: any, message: string): never {
|
||||
console.error(message, error); // Log detalhado do erro
|
||||
throw new InternalServerErrorException({
|
||||
message,
|
||||
sqlMessage: error.sqlMessage || error.message,
|
||||
sqlState: error.sqlState,
|
||||
});
|
||||
}
|
||||
|
||||
async sendImages(file: string) {
|
||||
// for (const file of files) {
|
||||
// const file = 'C:\\Temp\\brasil_2.jpg'
|
||||
if (file.endsWith(".jpg")) {
|
||||
const fileName = file; //directoryImages + '\\' + file;
|
||||
fs.readFile(fileName, (err, data) => {
|
||||
if (err) throw err;
|
||||
if (err) {
|
||||
console.log(`WRITE ERROR: ${err}`);
|
||||
} else {
|
||||
this.uploadS3(data, 'jur-saidaretornoveiculo', file.replace('./uploads/', ''));
|
||||
}
|
||||
});
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
async uploadS3(file, bucket, name) {
|
||||
const s3 = this.getS3();
|
||||
const params = {
|
||||
Bucket: bucket,
|
||||
Key: String(name),
|
||||
Body: file,
|
||||
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
s3.upload(params, (err, data) => {
|
||||
if (err) {
|
||||
console.log(JSON.stringify(err));
|
||||
reject(err.message);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getS3() {
|
||||
return new S3({
|
||||
accessKeyId: "AKIAVHJOO6W765ZT2PNI", //process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: "IFtP6Foc7JlE6TfR3psBAERUCMlH+4cRMx0GVIx2", // process.env.AWS_SECRET_ACCESS_KEY,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,44 +1,72 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/controllers#controllers
|
||||
*/
|
||||
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
|
||||
import { DataConsultService } from './data-consult.service';
|
||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'
|
||||
import { ProductDto } from './dto/product.dto';
|
||||
import { StoreDto } from './dto/store.dto';
|
||||
import { SellerDto } from './dto/seller.dto';
|
||||
import { BillingDto } from './dto/billing.dto';
|
||||
import { CustomerDto } from './dto/customer.dto';
|
||||
|
||||
@ApiTags('DataConsult')
|
||||
@Controller('api/v1/data-consult')
|
||||
export class DataConsultController {
|
||||
|
||||
constructor(private readonly dataConsultService: DataConsultService) {}
|
||||
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('stores')
|
||||
async stores() {
|
||||
@ApiOperation({ summary: 'Lista todas as lojas' })
|
||||
@ApiResponse({ status: 200, description: 'Lista de lojas retornada com sucesso', type: [StoreDto] })
|
||||
async stores(): Promise<StoreDto[]> {
|
||||
return this.dataConsultService.stores();
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('sellers')
|
||||
async sellers() {
|
||||
@ApiOperation({ summary: 'Lista todos os vendedores' })
|
||||
@ApiResponse({ status: 200, description: 'Lista de vendedores retornada com sucesso', type: [SellerDto] })
|
||||
async sellers(): Promise<SellerDto[]> {
|
||||
return this.dataConsultService.sellers();
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('billings')
|
||||
async billings() {
|
||||
@ApiOperation({ summary: 'Retorna informações de faturamento' })
|
||||
@ApiResponse({ status: 200, description: 'Informações de faturamento retornadas com sucesso', type: [BillingDto] })
|
||||
async billings(): Promise<BillingDto[]> {
|
||||
return this.dataConsultService.billings();
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('customers/:filter')
|
||||
async customer(@Param('filter') filter: string) {
|
||||
@ApiOperation({ summary: 'Filtra clientes pelo parâmetro fornecido' })
|
||||
@ApiParam({ name: 'filter', description: 'Filtro de busca para clientes' })
|
||||
@ApiResponse({ status: 200, description: 'Lista de clientes filtrados retornada com sucesso', type: [CustomerDto] })
|
||||
async customer(@Param('filter') filter: string): Promise<CustomerDto[]> {
|
||||
return this.dataConsultService.customers(filter);
|
||||
}
|
||||
|
||||
@Get('products/:filter')
|
||||
async products(@Param('filter') filter: string) {
|
||||
@ApiOperation({ summary: 'Busca produtos filtrados' })
|
||||
@ApiParam({ name: 'filter', description: 'Filtro de busca' })
|
||||
@ApiResponse({ status: 200, description: 'Lista de produtos filtrados retornada com sucesso', type: [ProductDto] })
|
||||
async products(@Param('filter') filter: string): Promise<ProductDto[]> {
|
||||
return this.dataConsultService.products(filter);
|
||||
}
|
||||
|
||||
|
||||
@Get('all')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'VIEW DE 500 PRODUTOS' })
|
||||
@ApiResponse({ status: 200, description: 'Lista de 500 produtos retornada com sucesso', type: [ProductDto] })
|
||||
async getAllProducts(): Promise<ProductDto[]> {
|
||||
return this.dataConsultService.getAllProducts();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DataConsultService } from './data-consult.service';
|
||||
import { DataConsultController } from './data-consult.controller';
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/modules
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DataConsultRepository } from './data-consult.repository';
|
||||
import { LoggerModule } from 'src/Log/logger.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { RedisModule } from 'src/core/configs/cache/redis.module';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [
|
||||
DataConsultController,],
|
||||
imports: [LoggerModule, ConfigModule, RedisModule],
|
||||
controllers: [DataConsultController],
|
||||
providers: [
|
||||
DataConsultService,],
|
||||
DataConsultService,
|
||||
DataConsultRepository,
|
||||
|
||||
],
|
||||
})
|
||||
export class DataConsultModule { }
|
||||
export class DataConsultModule {}
|
||||
|
||||
105
src/data-consult/data-consult.repository.ts
Normal file
105
src/data-consult/data-consult.repository.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { createOracleConfig } from '../core/configs/typeorm.oracle.config';
|
||||
import { StoreDto } from './dto/store.dto';
|
||||
import { SellerDto } from './dto/seller.dto';
|
||||
import { BillingDto } from './dto/billing.dto';
|
||||
import { CustomerDto } from './dto/customer.dto';
|
||||
import { ProductDto } from './dto/product.dto';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DATA_SOURCE } from '../core/constants';
|
||||
|
||||
@Injectable()
|
||||
export class DataConsultRepository {
|
||||
constructor(
|
||||
@Inject(DATA_SOURCE) private readonly dataSource: DataSource,
|
||||
private readonly configService: ConfigService
|
||||
) {}
|
||||
|
||||
private async executeQuery<T>(sql: string, params: any[] = []): Promise<T> {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
try {
|
||||
const result = await queryRunner.query(sql, params);
|
||||
return result as T;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
async findStores(): Promise<StoreDto[]> {
|
||||
const sql = `
|
||||
SELECT PCFILIAL.CODIGO as "id",
|
||||
PCFILIAL.RAZAOSOCIAL as "name",
|
||||
PCFILIAL.CODIGO || ' - ' || PCFILIAL.FANTASIA as "store"
|
||||
FROM PCFILIAL
|
||||
WHERE PCFILIAL.CODIGO NOT IN ('99', '69')
|
||||
ORDER BY TO_NUMBER(PCFILIAL.CODIGO)
|
||||
`;
|
||||
const results = await this.executeQuery<StoreDto[]>(sql);
|
||||
return results.map(result => new StoreDto(result));
|
||||
}
|
||||
|
||||
async findSellers(): Promise<SellerDto[]> {
|
||||
const sql = `
|
||||
SELECT PCUSUARI.CODUSUR as "id",
|
||||
PCUSUARI.NOME as "name"
|
||||
FROM PCUSUARI
|
||||
WHERE PCUSUARI.DTTERMINO IS NULL
|
||||
AND PCUSUARI.TIPOVEND NOT IN ('P')
|
||||
AND (PCUSUARI.BLOQUEIO IS NULL OR PCUSUARI.BLOQUEIO = 'N')
|
||||
`;
|
||||
const results = await this.executeQuery<SellerDto[]>(sql);
|
||||
return results.map(result => new SellerDto(result));
|
||||
}
|
||||
|
||||
async findBillings(): Promise<BillingDto[]> {
|
||||
const sql = `
|
||||
SELECT PCPEDC.NUMPED as "id",
|
||||
PCPEDC.DATA as "date",
|
||||
PCPEDC.VLTOTAL as "total"
|
||||
FROM PCPEDC
|
||||
WHERE PCPEDC.POSICAO = 'F'
|
||||
`;
|
||||
const results = await this.executeQuery<BillingDto[]>(sql);
|
||||
return results.map(result => new BillingDto(result));
|
||||
}
|
||||
|
||||
async findCustomers(filter: string): Promise<CustomerDto[]> {
|
||||
const sql = `
|
||||
SELECT PCCLIENT.CODCLI as "id",
|
||||
PCCLIENT.CLIENTE as "name",
|
||||
PCCLIENT.CGCENT as "document"
|
||||
FROM PCCLIENT
|
||||
WHERE PCCLIENT.CLIENTE LIKE :filter
|
||||
OR PCCLIENT.CGCENT LIKE :filter
|
||||
`;
|
||||
const results = await this.executeQuery<CustomerDto[]>(sql, [`%${filter}%`]);
|
||||
return results.map(result => new CustomerDto(result));
|
||||
}
|
||||
|
||||
async findProducts(filter: string): Promise<ProductDto[]> {
|
||||
const sql = `
|
||||
SELECT PCPRODUT.CODPROD as "id",
|
||||
PCPRODUT.DESCRICAO as "name",
|
||||
PCPRODUT.CODFAB as "manufacturerCode"
|
||||
FROM PCPRODUT
|
||||
WHERE PCPRODUT.DESCRICAO LIKE :filter
|
||||
OR PCPRODUT.CODFAB LIKE :filter
|
||||
`;
|
||||
const results = await this.executeQuery<ProductDto[]>(sql, [`%${filter}%`]);
|
||||
return results.map(result => new ProductDto(result));
|
||||
}
|
||||
|
||||
async findAllProducts(): Promise<ProductDto[]> {
|
||||
const sql = `
|
||||
SELECT PCPRODUT.CODPROD as "id",
|
||||
PCPRODUT.DESCRICAO as "name",
|
||||
PCPRODUT.CODFAB as "manufacturerCode"
|
||||
FROM PCPRODUT
|
||||
WHERE ROWNUM <= 500
|
||||
`;
|
||||
const results = await this.executeQuery<ProductDto[]>(sql);
|
||||
return results.map(result => new ProductDto(result));
|
||||
}
|
||||
}
|
||||
@@ -1,170 +1,137 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/*
|
||||
|
||||
https://docs.nestjs.com/providers#services
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { typeOrmConfig } from 'src/core/configs/typeorm.config';
|
||||
import { Injectable, HttpException, HttpStatus, Inject } from '@nestjs/common';
|
||||
import { DataConsultRepository } from './data-consult.repository';
|
||||
import { StoreDto } from './dto/store.dto';
|
||||
import { SellerDto } from './dto/seller.dto';
|
||||
import { BillingDto } from './dto/billing.dto';
|
||||
import { CustomerDto } from './dto/customer.dto';
|
||||
import { ProductDto } from './dto/product.dto';
|
||||
import { ILogger } from '../Log/ILogger';
|
||||
import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider';
|
||||
import { IRedisClient } from '../core/configs/cache/IRedisClient';
|
||||
import { getOrSetCache } from '../shared/cache.util';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { DATA_SOURCE } from '../core/constants';
|
||||
|
||||
@Injectable()
|
||||
export class DataConsultService {
|
||||
private readonly SELLERS_CACHE_KEY = 'data-consult:sellers';
|
||||
private readonly SELLERS_TTL = 3600;
|
||||
private readonly STORES_TTL = 3600;
|
||||
private readonly BILLINGS_TTL = 3600;
|
||||
private readonly ALL_PRODUCTS_CACHE_KEY = 'data-consult:products:all';
|
||||
private readonly ALL_PRODUCTS_TTL = 600;
|
||||
|
||||
constructor(
|
||||
private readonly repository: DataConsultRepository,
|
||||
@Inject(RedisClientToken) private readonly redisClient: IRedisClient,
|
||||
@Inject('LoggerService') private readonly logger: ILogger,
|
||||
@Inject(DATA_SOURCE) private readonly dataSource: DataSource
|
||||
) {}
|
||||
|
||||
async stores() {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
/**
|
||||
* Obter todas as lojas
|
||||
* @returns Array de StoreDto
|
||||
*/
|
||||
async stores(): Promise<StoreDto[]> {
|
||||
this.logger.log('Buscando todas as lojas');
|
||||
try {
|
||||
const sql = `SELECT PCFILIAL.CODIGO as "id"
|
||||
,PCFILIAL.RAZAOSOCIAL as "name"
|
||||
,PCFILIAL.CODIGO || ' - ' || PCFILIAL.FANTASIA as "store"
|
||||
FROM PCFILIAL
|
||||
WHERE PCFILIAL.CODIGO NOT IN ('99', '69')
|
||||
ORDER BY TO_NUMBER(PCFILIAL.CODIGO)`;
|
||||
|
||||
|
||||
const stores = await queryRunner.manager.query(sql);
|
||||
|
||||
return stores;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
const stores = await this.repository.findStores();
|
||||
return stores.map(store => new StoreDto(store));
|
||||
} catch (error) {
|
||||
this.logger.error('Erro ao buscar lojas', error);
|
||||
throw new HttpException('Erro ao buscar lojas', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async sellers() {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
/**
|
||||
* Obter todos os vendedores
|
||||
* @returns Array de SellerDto
|
||||
*/
|
||||
async sellers(): Promise<SellerDto[]> {
|
||||
this.logger.log('Buscando vendedores com cache Redis...');
|
||||
try {
|
||||
const sql = `SELECT PCUSUARI.CODUSUR as "id"
|
||||
,PCUSUARI.NOME as "name"
|
||||
FROM PCUSUARI
|
||||
WHERE PCUSUARI.DTTERMINO IS NULL
|
||||
AND PCUSUARI.TIPOVEND NOT IN ('P')
|
||||
ORDER BY PCUSUARI.NOME`;
|
||||
|
||||
|
||||
const sellers = await queryRunner.manager.query(sql);
|
||||
|
||||
return sellers;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
return getOrSetCache<SellerDto[]>(
|
||||
this.redisClient,
|
||||
this.SELLERS_CACHE_KEY,
|
||||
this.SELLERS_TTL,
|
||||
async () => {
|
||||
const sellers = await this.repository.findSellers();
|
||||
return sellers.map(seller => new SellerDto(seller));
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Erro ao buscar vendedores', error);
|
||||
throw new HttpException('Erro ao buscar vendedores', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
async billings() {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
/**
|
||||
* @returns Array de BillingDto
|
||||
*/
|
||||
async billings(): Promise<BillingDto[]> {
|
||||
this.logger.log('Buscando informações de faturamento');
|
||||
try {
|
||||
const sql = `SELECT PCCOB.CODCOB as "id"
|
||||
,PCCOB.CODCOB||' - '||PCCOB.COBRANCA as "description"
|
||||
FROM PCCOB
|
||||
WHERE PCCOB.CODCOB NOT IN ('DEVP', 'DEVT', 'DESD')
|
||||
ORDER BY PCCOB.COBRANCA`;
|
||||
|
||||
|
||||
const sellers = await queryRunner.manager.query(sql);
|
||||
|
||||
return sellers;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
const billings = await this.repository.findBillings();
|
||||
return billings.map(billing => new BillingDto(billing));
|
||||
} catch (error) {
|
||||
this.logger.error('Erro ao buscar faturamento', error);
|
||||
throw new HttpException('Erro ao buscar faturamento', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async customers(filter: string) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
/**
|
||||
* Obter clientes filtrados por termo de pesquisa
|
||||
* @param filter - Termo de pesquisa para filtrar clientes
|
||||
* @returns Array de CustomerDto
|
||||
*/
|
||||
async customers(filter: string): Promise<CustomerDto[]> {
|
||||
this.logger.log(`Buscando clientes com filtro: ${filter}`);
|
||||
try {
|
||||
let sql = `SELECT PCCLIENT.CODCLI as "id"
|
||||
,PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE||
|
||||
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
|
||||
FROM PCCLIENT
|
||||
WHERE PCCLIENT.CODCLI = REGEXP_REPLACE('${filter}', '[^0-9]', '')
|
||||
ORDER BY PCCLIENT.CLIENTE`;
|
||||
let customers = await queryRunner.manager.query(sql);
|
||||
|
||||
if (customers.length == 0) {
|
||||
sql = `SELECT PCCLIENT.CODCLI as "id"
|
||||
,PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE||
|
||||
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
|
||||
FROM PCCLIENT
|
||||
WHERE REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '') = REGEXP_REPLACE('${filter}', '[^0-9]', '')
|
||||
ORDER BY PCCLIENT.CLIENTE`;
|
||||
customers = await queryRunner.manager.query(sql);
|
||||
if (!filter || typeof filter !== 'string') {
|
||||
throw new HttpException('Filtro inválido', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (customers.length == 0) {
|
||||
sql = `SELECT PCCLIENT.CODCLI as "id"
|
||||
,PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE||
|
||||
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
|
||||
FROM PCCLIENT
|
||||
WHERE PCCLIENT.CLIENTE LIKE '${filter.toUpperCase().replace('@', '%')}%'
|
||||
ORDER BY PCCLIENT.CLIENTE`;
|
||||
customers = await queryRunner.manager.query(sql);
|
||||
}
|
||||
|
||||
return customers;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
const customers = await this.repository.findCustomers(filter);
|
||||
return customers.map(customer => new CustomerDto(customer));
|
||||
} catch (error) {
|
||||
this.logger.error('Erro ao buscar clientes', error);
|
||||
throw new HttpException('Erro ao buscar clientes', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async products(filter: string) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
/**
|
||||
* Obter produtos filtrados por termo de pesquisa
|
||||
* @param filter - Termo de pesquisa para filtrar produtos
|
||||
* @returns Array de ProductDto
|
||||
*/
|
||||
async products(filter: string): Promise<ProductDto[]> {
|
||||
this.logger.log(`Buscando produtos com filtro: ${filter}`);
|
||||
try {
|
||||
let sql = `SELECT PCPRODUT.CODPROD as "id"
|
||||
,PCPRODUT.CODPROD || ' - '|| PCPRODUT.DESCRICAO||
|
||||
' ( '||REGEXP_REPLACE(PCPRODUT.CODAUXILIAR, '[^0-9]', '')||' )' as "description"
|
||||
FROM PCPRODUT
|
||||
WHERE PCPRODUT.CODPROD = REGEXP_REPLACE('${filter}', '[^0-9]', '')
|
||||
ORDER BY PCPRODUT.DESCRICAO`;
|
||||
let products = await queryRunner.manager.query(sql);
|
||||
|
||||
if (products.length == 0) {
|
||||
sql = `SELECT PCPRODUT.CODPROD as "id"
|
||||
,PCPRODUT.CODPROD || ' - '|| PCPRODUT.DESCRICAO||
|
||||
' ( '||REGEXP_REPLACE(PCPRODUT.CODAUXILIAR, '[^0-9]', '')||' )' as "description"
|
||||
FROM PCPRODUT
|
||||
WHERE PCPRODUT.CODAUXILIAR = REGEXP_REPLACE('${filter}', '[^0-9]', '')
|
||||
ORDER BY PCPRODUT.DESCRICAO`;
|
||||
products = await queryRunner.manager.query(sql);
|
||||
if (!filter || typeof filter !== 'string') {
|
||||
throw new HttpException('Filtro inválido', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (products.length == 0) {
|
||||
sql = `SELECT PCPRODUT.CODPROD as "id"
|
||||
,PCPRODUT.CODPROD || ' - '|| PCPRODUT.DESCRICAO||
|
||||
' ( '||REGEXP_REPLACE(PCPRODUT.CODAUXILIAR, '[^0-9]', '')||' )' as "description"
|
||||
FROM PCPRODUT
|
||||
WHERE PCPRODUT.DESCRICAO LIKE '${filter}%'
|
||||
ORDER BY PCPRODUT.DESCRICAO`;
|
||||
products = await queryRunner.manager.query(sql);
|
||||
}
|
||||
|
||||
return products;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
const products = await this.repository.findProducts(filter);
|
||||
return products.map(product => new ProductDto(product));
|
||||
} catch (error) {
|
||||
this.logger.error('Erro ao buscar produtos', error);
|
||||
throw new HttpException('Erro ao buscar produtos', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getAllProducts(): Promise<ProductDto[]> {
|
||||
this.logger.log('Buscando todos os produtos');
|
||||
try {
|
||||
return getOrSetCache<ProductDto[]>(
|
||||
this.redisClient,
|
||||
this.ALL_PRODUCTS_CACHE_KEY,
|
||||
this.ALL_PRODUCTS_TTL,
|
||||
async () => {
|
||||
const products = await this.repository.findAllProducts();
|
||||
return products.map(product => new ProductDto(product));
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Erro ao buscar todos os produtos', error);
|
||||
throw new HttpException('Erro ao buscar produtos', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
src/data-consult/dto/billing.dto.ts
Normal file
16
src/data-consult/dto/billing.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class BillingDto {
|
||||
@ApiProperty({ description: 'Identificador do faturamento' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: 'Data do faturamento' })
|
||||
date: Date;
|
||||
|
||||
@ApiProperty({ description: 'Valor total do faturamento' })
|
||||
total: number;
|
||||
|
||||
constructor(partial: Partial<BillingDto>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
16
src/data-consult/dto/customer.dto.ts
Normal file
16
src/data-consult/dto/customer.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CustomerDto {
|
||||
@ApiProperty({ description: 'Identificador do cliente' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: 'Nome do cliente' })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: 'Documento do cliente' })
|
||||
document: string;
|
||||
|
||||
constructor(partial: Partial<CustomerDto>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
16
src/data-consult/dto/product.dto.ts
Normal file
16
src/data-consult/dto/product.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ProductDto {
|
||||
@ApiProperty({ description: 'Identificador do produto' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: 'Nome do produto' })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: 'Código do fabricante' })
|
||||
manufacturerCode: string;
|
||||
|
||||
constructor(partial: Partial<ProductDto>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
13
src/data-consult/dto/seller.dto.ts
Normal file
13
src/data-consult/dto/seller.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class SellerDto {
|
||||
@ApiProperty({ description: 'Identificador do vendedor' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: 'Nome do vendedor' })
|
||||
name: string;
|
||||
|
||||
constructor(partial: Partial<SellerDto>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
16
src/data-consult/dto/store.dto.ts
Normal file
16
src/data-consult/dto/store.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class StoreDto {
|
||||
@ApiProperty({ description: 'Identificador da loja' })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: 'Nome da loja' })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: 'Representação da loja (código e fantasia)' })
|
||||
store: string;
|
||||
|
||||
constructor(partial: Partial<StoreDto>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
258
src/health/alert/health-alert.service.ts
Normal file
258
src/health/alert/health-alert.service.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { HealthCheckResult } from '@nestjs/terminus';
|
||||
|
||||
@Injectable()
|
||||
export class HealthAlertService {
|
||||
private readonly logger = new Logger(HealthAlertService.name);
|
||||
private readonly webhookUrls: Record<string, string>;
|
||||
private readonly alertThresholds: Record<string, any>;
|
||||
private readonly alertCooldowns: Map<string, number> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
// Configurações de webhooks para diferentes canais de alerta
|
||||
this.webhookUrls = {
|
||||
slack: this.configService.get<string>('ALERT_WEBHOOK_SLACK'),
|
||||
teams: this.configService.get<string>('ALERT_WEBHOOK_TEAMS'),
|
||||
email: this.configService.get<string>('ALERT_WEBHOOK_EMAIL'),
|
||||
};
|
||||
|
||||
// Thresholds para diferentes tipos de alerta
|
||||
this.alertThresholds = {
|
||||
disk: {
|
||||
criticalPercent: this.configService.get<number>('ALERT_DISK_CRITICAL_PERCENT', 90),
|
||||
warningPercent: this.configService.get<number>('ALERT_DISK_WARNING_PERCENT', 80),
|
||||
},
|
||||
memory: {
|
||||
criticalPercent: this.configService.get<number>('ALERT_MEMORY_CRITICAL_PERCENT', 90),
|
||||
warningPercent: this.configService.get<number>('ALERT_MEMORY_WARNING_PERCENT', 80),
|
||||
},
|
||||
db: {
|
||||
cooldownMinutes: this.configService.get<number>('ALERT_DB_COOLDOWN_MINUTES', 15),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async processHealthCheckResult(result: HealthCheckResult): Promise<void> {
|
||||
try {
|
||||
const { status, info, error, details } = result;
|
||||
|
||||
// Se o status geral não for 'ok', envie um alerta
|
||||
if (status !== 'ok') {
|
||||
// Verificar quais componentes estão com problema
|
||||
const failedComponents = Object.entries(error)
|
||||
.map(([key, value]) => ({ key, value }));
|
||||
|
||||
if (failedComponents.length > 0) {
|
||||
await this.sendAlert('critical', 'Health Check Falhou',
|
||||
`Os seguintes componentes estão com problemas: ${failedComponents.map(c => c.key).join(', ')}`,
|
||||
{ result, failedComponents }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar alertas específicos para cada tipo de componente
|
||||
if (details.disk) {
|
||||
await this.checkDiskAlerts(details.disk);
|
||||
}
|
||||
|
||||
if (details.memory_heap) {
|
||||
await this.checkMemoryAlerts(details.memory_heap);
|
||||
}
|
||||
|
||||
// Verificar alertas de banco de dados
|
||||
['oracle', 'postgres'].forEach(db => {
|
||||
if (details[db] && details[db].status !== 'up') {
|
||||
this.checkDatabaseAlerts(db, details[db]);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao processar health check result: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkDiskAlerts(diskDetails: any): Promise<void> {
|
||||
try {
|
||||
if (!diskDetails.freeBytes || !diskDetails.totalBytes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usedPercent = ((diskDetails.totalBytes - diskDetails.freeBytes) / diskDetails.totalBytes) * 100;
|
||||
|
||||
if (usedPercent >= this.alertThresholds.disk.criticalPercent) {
|
||||
await this.sendAlert('critical', 'Espaço em Disco Crítico',
|
||||
`O uso de disco está em ${usedPercent.toFixed(1)}%, acima do limite crítico de ${this.alertThresholds.disk.criticalPercent}%`,
|
||||
{ diskDetails, usedPercent }
|
||||
);
|
||||
} else if (usedPercent >= this.alertThresholds.disk.warningPercent) {
|
||||
await this.sendAlert('warning', 'Alerta de Espaço em Disco',
|
||||
`O uso de disco está em ${usedPercent.toFixed(1)}%, acima do limite de alerta de ${this.alertThresholds.disk.warningPercent}%`,
|
||||
{ diskDetails, usedPercent }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao verificar alertas de disco: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkMemoryAlerts(memoryDetails: any): Promise<void> {
|
||||
try {
|
||||
if (!memoryDetails.usedBytes || !memoryDetails.thresholdBytes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usedPercent = (memoryDetails.usedBytes / memoryDetails.thresholdBytes) * 100;
|
||||
|
||||
if (usedPercent >= this.alertThresholds.memory.criticalPercent) {
|
||||
await this.sendAlert('critical', 'Uso de Memória Crítico',
|
||||
`O uso de memória heap está em ${usedPercent.toFixed(1)}%, acima do limite crítico de ${this.alertThresholds.memory.criticalPercent}%`,
|
||||
{ memoryDetails, usedPercent }
|
||||
);
|
||||
} else if (usedPercent >= this.alertThresholds.memory.warningPercent) {
|
||||
await this.sendAlert('warning', 'Alerta de Uso de Memória',
|
||||
`O uso de memória heap está em ${usedPercent.toFixed(1)}%, acima do limite de alerta de ${this.alertThresholds.memory.warningPercent}%`,
|
||||
{ memoryDetails, usedPercent }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao verificar alertas de memória: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkDatabaseAlerts(dbName: string, dbDetails: any): Promise<void> {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const lastAlertTime = this.alertCooldowns.get(dbName) || 0;
|
||||
const cooldownMs = this.alertThresholds.db.cooldownMinutes * 60 * 1000;
|
||||
|
||||
// Verifica se já passou o período de cooldown para este banco
|
||||
if (now - lastAlertTime >= cooldownMs) {
|
||||
await this.sendAlert('critical', `Problema de Conexão com Banco de Dados ${dbName}`,
|
||||
`A conexão com o banco de dados ${dbName} está com problemas: ${dbDetails.message || 'Erro não especificado'}`,
|
||||
{ dbName, dbDetails }
|
||||
);
|
||||
|
||||
// Atualiza o timestamp do último alerta
|
||||
this.alertCooldowns.set(dbName, now);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao verificar alertas de banco de dados: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendAlert(
|
||||
severity: 'critical' | 'warning' | 'info',
|
||||
title: string,
|
||||
message: string,
|
||||
details?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const environment = this.configService.get<string>('NODE_ENV', 'development');
|
||||
const appName = this.configService.get<string>('APP_NAME', 'Portal Jurunense API');
|
||||
|
||||
this.logger.warn(`[${severity.toUpperCase()}] ${title}: ${message}`);
|
||||
|
||||
const payload = {
|
||||
severity,
|
||||
title: `[${environment.toUpperCase()}] [${appName}] ${title}`,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: details || {},
|
||||
environment,
|
||||
};
|
||||
|
||||
// Enviar para Slack, se configurado
|
||||
if (this.webhookUrls.slack) {
|
||||
await this.sendSlackAlert(payload);
|
||||
}
|
||||
|
||||
// Enviar para Microsoft Teams, se configurado
|
||||
if (this.webhookUrls.teams) {
|
||||
await this.sendTeamsAlert(payload);
|
||||
}
|
||||
|
||||
// Enviar para serviço de email, se configurado
|
||||
if (this.webhookUrls.email) {
|
||||
await this.sendEmailAlert(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao enviar alerta: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendSlackAlert(payload: any): Promise<void> {
|
||||
try {
|
||||
const slackPayload = {
|
||||
text: `${payload.title}`,
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: payload.title,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `*Mensagem:* ${payload.message}\n*Severidade:* ${payload.severity}\n*Ambiente:* ${payload.environment}\n*Timestamp:* ${payload.timestamp}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await firstValueFrom(this.httpService.post(this.webhookUrls.slack, slackPayload));
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao enviar alerta para Slack: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendTeamsAlert(payload: any): Promise<void> {
|
||||
try {
|
||||
const teamsPayload = {
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"themeColor": payload.severity === 'critical' ? "FF0000" : (payload.severity === 'warning' ? "FFA500" : "0078D7"),
|
||||
"summary": payload.title,
|
||||
"sections": [
|
||||
{
|
||||
"activityTitle": payload.title,
|
||||
"activitySubtitle": `Severidade: ${payload.severity} | Ambiente: ${payload.environment}`,
|
||||
"text": payload.message,
|
||||
"facts": [
|
||||
{
|
||||
"name": "Timestamp",
|
||||
"value": payload.timestamp
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await firstValueFrom(this.httpService.post(this.webhookUrls.teams, teamsPayload));
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao enviar alerta para Microsoft Teams: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendEmailAlert(payload: any): Promise<void> {
|
||||
try {
|
||||
const emailPayload = {
|
||||
subject: payload.title,
|
||||
text: `${payload.message}\n\nSeveridade: ${payload.severity}\nAmbiente: ${payload.environment}\nTimestamp: ${payload.timestamp}`,
|
||||
html: `<h2>${payload.title}</h2><p>${payload.message}</p><p><strong>Severidade:</strong> ${payload.severity}<br><strong>Ambiente:</strong> ${payload.environment}<br><strong>Timestamp:</strong> ${payload.timestamp}</p>`,
|
||||
};
|
||||
|
||||
await firstValueFrom(this.httpService.post(this.webhookUrls.email, emailPayload));
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao enviar alerta por email: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/health/health.controller.ts
Normal file
109
src/health/health.controller.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import {
|
||||
HealthCheck,
|
||||
HealthCheckService,
|
||||
HttpHealthIndicator,
|
||||
DiskHealthIndicator,
|
||||
MemoryHealthIndicator,
|
||||
} from '@nestjs/terminus';
|
||||
import { TypeOrmHealthIndicator } from './indicators/typeorm.health';
|
||||
import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import * as os from 'os';
|
||||
|
||||
@ApiTags('Health Check')
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
private readonly diskPath: string;
|
||||
|
||||
constructor(
|
||||
private health: HealthCheckService,
|
||||
private http: HttpHealthIndicator,
|
||||
private disk: DiskHealthIndicator,
|
||||
private memory: MemoryHealthIndicator,
|
||||
private typeOrmHealth: TypeOrmHealthIndicator,
|
||||
private dbPoolStats: DbPoolStatsIndicator,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
// Define o caminho correto para o disco, baseado no sistema operacional
|
||||
this.diskPath = os.platform() === 'win32' ? 'C:\\' : '/';
|
||||
}
|
||||
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: 'Verificar saúde geral da aplicação' })
|
||||
check() {
|
||||
return this.health.check([
|
||||
// Verifica o status da própria aplicação
|
||||
() => this.http.pingCheck('api', 'http://localhost:8066/docs'),
|
||||
|
||||
// Verifica espaço em disco (espaço livre < 80%)
|
||||
() => this.disk.checkStorage('disk_percent', {
|
||||
path: this.diskPath,
|
||||
thresholdPercent: 0.8, // 80%
|
||||
}),
|
||||
|
||||
// Verifica espaço em disco (pelo menos 500MB livres)
|
||||
() => this.disk.checkStorage('disk_space', {
|
||||
path: this.diskPath,
|
||||
threshold: 500 * 1024 * 1024, // 500MB em bytes
|
||||
}),
|
||||
|
||||
// Verifica uso de memória (heap <150MB)
|
||||
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024), // 150MB
|
||||
|
||||
// Verifica as conexões de banco de dados
|
||||
() => this.typeOrmHealth.checkOracle(),
|
||||
() => this.typeOrmHealth.checkPostgres(),
|
||||
]);
|
||||
}
|
||||
|
||||
@Get('db')
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: 'Verificar saúde das conexões de banco de dados' })
|
||||
checkDatabase() {
|
||||
return this.health.check([
|
||||
() => this.typeOrmHealth.checkOracle(),
|
||||
() => this.typeOrmHealth.checkPostgres(),
|
||||
]);
|
||||
}
|
||||
|
||||
@Get('memory')
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: 'Verificar uso de memória' })
|
||||
checkMemory() {
|
||||
return this.health.check([
|
||||
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),
|
||||
() => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),
|
||||
]);
|
||||
}
|
||||
|
||||
@Get('disk')
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: 'Verificar espaço em disco' })
|
||||
checkDisk() {
|
||||
return this.health.check([
|
||||
// Verificar espaço em disco usando porcentagem
|
||||
() => this.disk.checkStorage('disk_percent', {
|
||||
path: this.diskPath,
|
||||
thresholdPercent: 0.8,
|
||||
}),
|
||||
// Verificar espaço em disco usando valor absoluto
|
||||
() => this.disk.checkStorage('disk_space', {
|
||||
path: this.diskPath,
|
||||
threshold: 500 * 1024 * 1024,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@Get('pool')
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: 'Verificar estatísticas do pool de conexões' })
|
||||
checkPoolStats() {
|
||||
return this.health.check([
|
||||
() => this.dbPoolStats.checkOraclePoolStats(),
|
||||
() => this.dbPoolStats.checkPostgresPoolStats(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
44
src/health/health.module.ts
Normal file
44
src/health/health.module.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { HealthController } from './health.controller';
|
||||
import { TypeOrmHealthIndicator } from './indicators/typeorm.health';
|
||||
import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
|
||||
import { metricProviders } from './metrics/metrics.config';
|
||||
import { CustomMetricsService } from './metrics/custom.metrics';
|
||||
import { MetricsInterceptor } from './metrics/metrics.interceptor';
|
||||
import { HealthAlertService } from './alert/health-alert.service';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TerminusModule,
|
||||
HttpModule,
|
||||
ConfigModule,
|
||||
PrometheusModule.register({
|
||||
path: '/metrics',
|
||||
defaultMetrics: {
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
controllers: [HealthController],
|
||||
providers: [
|
||||
TypeOrmHealthIndicator,
|
||||
DbPoolStatsIndicator,
|
||||
CustomMetricsService,
|
||||
HealthAlertService,
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: MetricsInterceptor,
|
||||
},
|
||||
...metricProviders,
|
||||
],
|
||||
exports: [
|
||||
CustomMetricsService,
|
||||
HealthAlertService,
|
||||
],
|
||||
})
|
||||
export class HealthModule {}
|
||||
193
src/health/indicators/db-pool-stats.health.ts
Normal file
193
src/health/indicators/db-pool-stats.health.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
HealthIndicator,
|
||||
HealthIndicatorResult,
|
||||
HealthCheckError, // Import HealthCheckError for better terminus integration
|
||||
} from '@nestjs/terminus';
|
||||
import { InjectConnection } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const ORACLE_HEALTH_KEY = 'oracle_pool_stats';
|
||||
const POSTGRES_HEALTH_KEY = 'postgres_pool_stats';
|
||||
const ORACLE_PROGRAM_PATTERN = 'node%'; // Default pattern for Oracle
|
||||
const POSTGRES_APP_NAME_PATTERN = 'nodejs%'; // Default pattern for PostgreSQL
|
||||
|
||||
@Injectable()
|
||||
export class DbPoolStatsIndicator extends HealthIndicator {
|
||||
private readonly logger = new Logger(DbPoolStatsIndicator.name);
|
||||
|
||||
constructor(
|
||||
|
||||
@InjectConnection('oracle') private readonly oracleDataSource: DataSource,
|
||||
@InjectConnection('postgres') private readonly postgresDataSource: DataSource,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica a integridade do pool de conexões Oracle consultando V$SESSION.
|
||||
* Observações: Requer privilégios SELECT em V$SESSION e depende da coluna PROGRAM.
|
||||
* Isso verifica principalmente a acessibilidade do banco de dados e o sucesso da execução da consulta.
|
||||
* Considere estatísticas de pool em nível de driver para obter uma integridade de pool mais precisa, se disponível.
|
||||
*
|
||||
* @param key Custom key for the health indicator component.
|
||||
* @param programLike Optional pattern to match the PROGRAM column in V$SESSION.
|
||||
*/
|
||||
async checkOraclePoolStats(
|
||||
key: string = ORACLE_HEALTH_KEY,
|
||||
programLike: string = ORACLE_PROGRAM_PATTERN,
|
||||
): Promise<HealthIndicatorResult> {
|
||||
try {
|
||||
// Usar parâmetros de consulta é uma boa prática, embora menos crítica para LIKE com um padrão fixo.
|
||||
// Oracle usa a sintaxe :paramName
|
||||
const query = `
|
||||
SELECT
|
||||
COUNT(*) AS "totalConnections" -- Use quoted identifiers if needed, or match case below
|
||||
FROM
|
||||
V$SESSION
|
||||
WHERE
|
||||
TYPE = 'USER'
|
||||
AND PROGRAM LIKE :pattern
|
||||
`;
|
||||
const params = { pattern: programLike };
|
||||
|
||||
const results: { totalConnections: number | string }[] =
|
||||
await this.oracleDataSource.query(query, [params.pattern]); // Pass parameters as an array for Oracle usually
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
this.logger.warn(`Oracle V$SESSION query returned no results for pattern '${programLike}'`);
|
||||
|
||||
}
|
||||
|
||||
const totalConnections = parseInt(String(results?.[0]?.totalConnections ?? 0), 10);
|
||||
|
||||
if (isNaN(totalConnections)) {
|
||||
throw new Error('Failed to parse totalConnections from Oracle V$SESSION query result.');
|
||||
}
|
||||
// isHealthy é verdadeiro se a consulta for executada sem gerar um erro.
|
||||
// Adicione lógica aqui se contagens de conexão específicas indicarem estado não íntegro (por exemplo, > poolMax)
|
||||
const isHealthy = true;
|
||||
const details = {
|
||||
totalConnections: totalConnections,
|
||||
programPattern: programLike,
|
||||
};
|
||||
|
||||
return this.getStatus(key, isHealthy, details);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Oracle pool stats check failed for key "${key}": ${error.message}`, error.stack);
|
||||
throw new HealthCheckError(
|
||||
`${key} check failed`,
|
||||
this.getStatus(key, false, { message: error.message }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica a integridade do pool de conexões do PostgreSQL consultando pg_stat_activity.
|
||||
* Observações: Depende de o application_name estar definido corretamente na string de conexão ou nas opções.
|
||||
* Isso verifica principalmente a acessibilidade do banco de dados e o sucesso da execução da consulta.
|
||||
* Considere estatísticas de pool em nível de driver para obter uma integridade de pool mais precisa, se disponível.
|
||||
*
|
||||
* @param key Custom key for the health indicator component.
|
||||
* @param appNameLike Optional pattern to match the application_name column.
|
||||
*/
|
||||
async checkPostgresPoolStats(
|
||||
key: string = POSTGRES_HEALTH_KEY,
|
||||
appNameLike: string = POSTGRES_APP_NAME_PATTERN,
|
||||
): Promise<HealthIndicatorResult> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
count(*) AS "totalConnections",
|
||||
sum(CASE WHEN state = 'active' THEN 1 ELSE 0 END) AS "activeConnections",
|
||||
sum(CASE WHEN state = 'idle' THEN 1 ELSE 0 END) AS "idleConnections",
|
||||
sum(CASE WHEN state = 'idle in transaction' THEN 1 ELSE 0 END) AS "idleInTransactionConnections"
|
||||
FROM
|
||||
pg_stat_activity
|
||||
WHERE
|
||||
datname = current_database()
|
||||
AND application_name LIKE $1
|
||||
`;
|
||||
const params = [appNameLike];
|
||||
|
||||
const results: {
|
||||
totalConnections: string | number;
|
||||
activeConnections: string | number;
|
||||
idleConnections: string | number;
|
||||
idleInTransactionConnections: string | number;
|
||||
}[] = await this.postgresDataSource.query(query, params);
|
||||
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
|
||||
throw new Error('PostgreSQL pg_stat_activity query returned no results unexpectedly.');
|
||||
}
|
||||
|
||||
const result = results[0];
|
||||
|
||||
const totalConnections = parseInt(String(result.totalConnections ?? 0), 10);
|
||||
const activeConnections = parseInt(String(result.activeConnections ?? 0), 10);
|
||||
const idleConnections = parseInt(String(result.idleConnections ?? 0), 10);
|
||||
const idleInTransactionConnections = parseInt(String(result.idleInTransactionConnections ?? 0), 10);
|
||||
|
||||
// Validate parsing
|
||||
if (isNaN(totalConnections) || isNaN(activeConnections) || isNaN(idleConnections) || isNaN(idleInTransactionConnections)) {
|
||||
throw new Error('Failed to parse connection counts from PostgreSQL pg_stat_activity query result.');
|
||||
}
|
||||
|
||||
|
||||
const isHealthy = true;
|
||||
const details = {
|
||||
totalConnections,
|
||||
activeConnections,
|
||||
idleConnections,
|
||||
idleInTransactionConnections,
|
||||
applicationNamePattern: appNameLike,
|
||||
};
|
||||
|
||||
return this.getStatus(key, isHealthy, details);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`PostgreSQL pool stats check failed for key "${key}": ${error.message}`, error.stack);
|
||||
|
||||
throw new HealthCheckError(
|
||||
`${key} check failed`,
|
||||
this.getStatus(key, false, { message: error.message }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to run all pool checks defined in this indicator.
|
||||
* You would typically call this from your main HealthController.
|
||||
*/
|
||||
async checkAllPools() : Promise<HealthIndicatorResult[]> {
|
||||
const results = await Promise.allSettled([
|
||||
this.checkOraclePoolStats(),
|
||||
this.checkPostgresPoolStats()
|
||||
]);
|
||||
|
||||
// Processa os resultados para se ajustar à estrutura do Terminus, se necessário, ou retorna diretamente
|
||||
// Observações: Métodos individuais já retornam HealthIndicatorResult ou lançam HealthCheckError
|
||||
// Este método pode não ser estritamente necessário se você chamar verificações individuais no controlador.
|
||||
// Para simplificar, vamos supor que o controlador chama as verificações individuais.
|
||||
// Se você quisesse que esse método retornasse um único status, precisaria de mais lógica.
|
||||
// Relançar erros ou agregar status.
|
||||
|
||||
// Example: Log results (individual methods handle the Terminus return/error)
|
||||
results.forEach(result => {
|
||||
if (result.status === 'rejected') {
|
||||
// Already logged and thrown as HealthCheckError inside the check methods
|
||||
} else {
|
||||
// Optionally log success details
|
||||
this.logger.log(`Pool check successful: ${JSON.stringify(result.value)}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return results
|
||||
.filter((r): r is PromiseFulfilledResult<HealthIndicatorResult> => r.status === 'fulfilled')
|
||||
.map(r => r.value);
|
||||
}
|
||||
}
|
||||
52
src/health/indicators/typeorm.health.ts
Normal file
52
src/health/indicators/typeorm.health.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
|
||||
import { InjectConnection } from '@nestjs/typeorm';
|
||||
import { Connection, DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class TypeOrmHealthIndicator extends HealthIndicator {
|
||||
constructor(
|
||||
@InjectConnection('oracle') private oracleConnection: DataSource,
|
||||
@InjectConnection('postgres') private postgresConnection: DataSource,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async checkOracle(): Promise<HealthIndicatorResult> {
|
||||
const key = 'oracle';
|
||||
|
||||
try {
|
||||
const isHealthy = this.oracleConnection.isInitialized;
|
||||
|
||||
const result = this.getStatus(key, isHealthy);
|
||||
|
||||
if (isHealthy) {
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new HealthCheckError('Oracle healthcheck failed', result);
|
||||
} catch (error) {
|
||||
const result = this.getStatus(key, false, { message: error.message });
|
||||
throw new HealthCheckError('Oracle healthcheck failed', result);
|
||||
}
|
||||
}
|
||||
|
||||
async checkPostgres(): Promise<HealthIndicatorResult> {
|
||||
const key = 'postgres';
|
||||
|
||||
try {
|
||||
const isHealthy = this.postgresConnection.isInitialized;
|
||||
|
||||
const result = this.getStatus(key, isHealthy);
|
||||
|
||||
if (isHealthy) {
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new HealthCheckError('Postgres healthcheck failed', result);
|
||||
} catch (error) {
|
||||
const result = this.getStatus(key, false, { message: error.message });
|
||||
throw new HealthCheckError('Postgres healthcheck failed', result);
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/health/metrics/custom.metrics.ts
Normal file
93
src/health/metrics/custom.metrics.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectMetric } from '@willsoto/nestjs-prometheus';
|
||||
import { Counter, Gauge, Histogram } from 'prom-client';
|
||||
import { InjectConnection } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class CustomMetricsService {
|
||||
constructor(
|
||||
@InjectMetric('http_request_total')
|
||||
private readonly requestCounter: Counter<string>,
|
||||
|
||||
@InjectMetric('http_request_duration_seconds')
|
||||
private readonly requestDuration: Histogram<string>,
|
||||
|
||||
@InjectMetric('api_memory_usage_bytes')
|
||||
private readonly memoryGauge: Gauge<string>,
|
||||
|
||||
@InjectMetric('api_db_connection_pool_used')
|
||||
private readonly dbPoolUsedGauge: Gauge<string>,
|
||||
|
||||
@InjectMetric('api_db_connection_pool_total')
|
||||
private readonly dbPoolTotalGauge: Gauge<string>,
|
||||
|
||||
@InjectMetric('api_db_query_duration_seconds')
|
||||
private readonly dbQueryDuration: Histogram<string>,
|
||||
|
||||
@InjectConnection('oracle')
|
||||
private oracleConnection: DataSource,
|
||||
|
||||
@InjectConnection('postgres')
|
||||
private postgresConnection: DataSource,
|
||||
) {
|
||||
// Iniciar coleta de métricas de memória
|
||||
this.startMemoryMetrics();
|
||||
|
||||
// Iniciar coleta de métricas do pool de conexões
|
||||
this.startDbPoolMetrics();
|
||||
}
|
||||
|
||||
recordHttpRequest(method: string, route: string, statusCode: number): void {
|
||||
this.requestCounter.inc({ method, route, statusCode: statusCode.toString() });
|
||||
}
|
||||
|
||||
startTimingRequest(): (labels?: Record<string, string>) => void {
|
||||
const end = this.requestDuration.startTimer();
|
||||
return (labels?: Record<string, string>) => end(labels);
|
||||
}
|
||||
|
||||
recordDbQueryDuration(db: 'oracle' | 'postgres', operation: string, durationMs: number): void {
|
||||
this.dbQueryDuration.observe({ db, operation }, durationMs / 1000);
|
||||
}
|
||||
|
||||
private startMemoryMetrics(): void {
|
||||
// Coletar métricas de memória a cada 15 segundos
|
||||
setInterval(() => {
|
||||
const memoryUsage = process.memoryUsage();
|
||||
this.memoryGauge.set({ type: 'rss' }, memoryUsage.rss);
|
||||
this.memoryGauge.set({ type: 'heapTotal' }, memoryUsage.heapTotal);
|
||||
this.memoryGauge.set({ type: 'heapUsed' }, memoryUsage.heapUsed);
|
||||
this.memoryGauge.set({ type: 'external' }, memoryUsage.external);
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
private startDbPoolMetrics(): void {
|
||||
// Coletar métricas do pool de conexões a cada 15 segundos
|
||||
setInterval(async () => {
|
||||
try {
|
||||
// Tente obter estatísticas do pool do Oracle
|
||||
// Nota: depende da implementação específica do OracleDB
|
||||
if (this.oracleConnection && this.oracleConnection.driver) {
|
||||
const oraclePoolStats = (this.oracleConnection.driver as any).pool?.getStatistics?.();
|
||||
if (oraclePoolStats) {
|
||||
this.dbPoolUsedGauge.set({ db: 'oracle' }, oraclePoolStats.busy || 0);
|
||||
this.dbPoolTotalGauge.set({ db: 'oracle' }, oraclePoolStats.poolMax || 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Tente obter estatísticas do pool do Postgres
|
||||
// Nota: depende da implementação específica do TypeORM
|
||||
if (this.postgresConnection && this.postgresConnection.driver) {
|
||||
const pgPoolStats = (this.postgresConnection.driver as any).pool;
|
||||
if (pgPoolStats) {
|
||||
this.dbPoolUsedGauge.set({ db: 'postgres' }, pgPoolStats.totalCount - pgPoolStats.idleCount || 0);
|
||||
this.dbPoolTotalGauge.set({ db: 'postgres' }, pgPoolStats.totalCount || 0);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao coletar métricas do pool de conexões:', error);
|
||||
}
|
||||
}, 15000);
|
||||
}
|
||||
}
|
||||
51
src/health/metrics/metrics.config.ts
Normal file
51
src/health/metrics/metrics.config.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
makeCounterProvider,
|
||||
makeGaugeProvider,
|
||||
makeHistogramProvider
|
||||
} from '@willsoto/nestjs-prometheus';
|
||||
|
||||
export const metricProviders = [
|
||||
// Contador de requisições HTTP
|
||||
makeCounterProvider({
|
||||
name: 'http_request_total',
|
||||
help: 'Total de requisições HTTP',
|
||||
labelNames: ['method', 'route', 'statusCode'],
|
||||
}),
|
||||
|
||||
// Histograma de duração de requisições HTTP
|
||||
makeHistogramProvider({
|
||||
name: 'http_request_duration_seconds',
|
||||
help: 'Duração das requisições HTTP em segundos',
|
||||
labelNames: ['method', 'route', 'error'], // 👈 adicionado "error"
|
||||
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
|
||||
}),
|
||||
|
||||
// Gauge para uso de memória
|
||||
makeGaugeProvider({
|
||||
name: 'api_memory_usage_bytes',
|
||||
help: 'Uso de memória da aplicação em bytes',
|
||||
labelNames: ['type'],
|
||||
}),
|
||||
|
||||
// Gauge para conexões de banco de dados usadas
|
||||
makeGaugeProvider({
|
||||
name: 'api_db_connection_pool_used',
|
||||
help: 'Número de conexões de banco de dados em uso',
|
||||
labelNames: ['db'],
|
||||
}),
|
||||
|
||||
// Gauge para total de conexões no pool de banco de dados
|
||||
makeGaugeProvider({
|
||||
name: 'api_db_connection_pool_total',
|
||||
help: 'Número total de conexões no pool de banco de dados',
|
||||
labelNames: ['db'],
|
||||
}),
|
||||
|
||||
// Histograma para duração de consultas de banco de dados
|
||||
makeHistogramProvider({
|
||||
name: 'api_db_query_duration_seconds',
|
||||
help: 'Duração das consultas de banco de dados em segundos',
|
||||
labelNames: ['db', 'operation'],
|
||||
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2],
|
||||
}),
|
||||
];
|
||||
64
src/health/metrics/metrics.interceptor.ts
Normal file
64
src/health/metrics/metrics.interceptor.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { CustomMetricsService } from './custom.metrics';
|
||||
|
||||
@Injectable()
|
||||
export class MetricsInterceptor implements NestInterceptor {
|
||||
constructor(private metricsService: CustomMetricsService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
if (context.getType() !== 'http') {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const { method, url } = request;
|
||||
|
||||
// Simplificar a rota para evitar cardinalidade alta no Prometheus
|
||||
// Ex: /users/123 -> /users/:id
|
||||
const route = this.normalizeRoute(url);
|
||||
|
||||
// Inicia o timer para medir a duração da requisição
|
||||
const endTimer = this.metricsService.startTimingRequest();
|
||||
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: (data) => {
|
||||
const response = context.switchToHttp().getResponse();
|
||||
const statusCode = response.statusCode;
|
||||
|
||||
// Registra a requisição concluída
|
||||
this.metricsService.recordHttpRequest(method, route, statusCode);
|
||||
|
||||
// Finaliza o timer com labels adicionais
|
||||
endTimer({ method, route });
|
||||
},
|
||||
error: (error) => {
|
||||
// Determina o código de status do erro
|
||||
const statusCode = error.status || 500;
|
||||
|
||||
// Registra a requisição com erro
|
||||
this.metricsService.recordHttpRequest(method, route, statusCode);
|
||||
|
||||
// Finaliza o timer com labels adicionais
|
||||
endTimer({ method, route, error: 'true' });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeRoute(url: string): string {
|
||||
// Remove query parameters
|
||||
const path = url.split('?')[0];
|
||||
|
||||
// Normaliza rotas com IDs e outros parâmetros dinâmicos
|
||||
// Por exemplo, /users/123 -> /users/:id
|
||||
return path.replace(/\/[0-9a-f]{8,}|\/[0-9]+/g, '/:id');
|
||||
}
|
||||
}
|
||||
@@ -4,48 +4,62 @@
|
||||
https://docs.nestjs.com/controllers#controllers
|
||||
*/
|
||||
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { LogisticService } from './logistic.service';
|
||||
import { CarOutDelivery } from 'src/core/models/car-out-delivery.model';
|
||||
import { CarInDelivery } from 'src/core/models/car-in-delivery.model';
|
||||
import { CarOutDelivery } from '../core/models/car-out-delivery.model';
|
||||
import { CarInDelivery } from '../core/models/car-in-delivery.model';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||
import { ApiBearerAuth, ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
|
||||
@Controller('api/v1/logistic')
|
||||
export class LogisticController {
|
||||
|
||||
constructor( private readonly logisticService: LogisticService) {}
|
||||
@ApiTags('Logística')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('api/v1/logistic')
|
||||
export class LogisticController {
|
||||
constructor(private readonly logisticService: LogisticService) {}
|
||||
|
||||
@Get('expedicao')
|
||||
@ApiOperation({ summary: 'Retorna informações de expedição' })
|
||||
getExpedicao() {
|
||||
try {
|
||||
return this.logisticService.getExpedicao();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('employee')
|
||||
@ApiOperation({ summary: 'Retorna lista de funcionários' })
|
||||
getEmployee() {
|
||||
return this.logisticService.getEmployee();
|
||||
}
|
||||
|
||||
@Get('deliveries/:placa')
|
||||
@ApiOperation({ summary: 'Retorna entregas por placa' })
|
||||
getDelivery(@Param('placa') placa: string) {
|
||||
return this.logisticService.getDeliveries(placa);
|
||||
}
|
||||
|
||||
@Get('status-car/:placa')
|
||||
@ApiOperation({ summary: 'Retorna status do veículo por placa' })
|
||||
getStatusCar(@Param('placa') placa: string) {
|
||||
return this.logisticService.getStatusCar(placa);
|
||||
}
|
||||
|
||||
@Post('create')
|
||||
@ApiOperation({ summary: 'Registra saída de veículo' })
|
||||
createOutCar(@Body() data: CarOutDelivery) {
|
||||
return this.logisticService.createCarOut(data);
|
||||
}
|
||||
|
||||
@Post('return-car')
|
||||
@ApiOperation({ summary: 'Registra retorno de veículo' })
|
||||
createinCar(@Body() data: CarInDelivery) {
|
||||
return this.logisticService.createCarIn(data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import { Length } from './../../node_modules/aws-sdk/clients/quicksight.d';
|
||||
import { count } from './../../node_modules/aws-sdk/clients/health.d';
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
|
||||
import { Get, HttpException, HttpStatus, Injectable, Query, UseGuards } from '@nestjs/common';
|
||||
import { stringify } from 'querystring';
|
||||
import { typeOrmConfig, typeOrmPgConfig } from 'src/core/configs/typeorm.config';
|
||||
import { CarOutDelivery } from 'src/core/models/car-out-delivery.model';
|
||||
import { BaseService } from 'src/core/services/base.service';
|
||||
import { createOracleConfig } from '../core/configs/typeorm.oracle.config';
|
||||
import { createPostgresConfig } from '../core/configs/typeorm.postgres.config';
|
||||
import { CarOutDelivery } from '../core/models/car-out-delivery.model';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { CarInDelivery } from 'src/core/models/car-in-delivery.model';
|
||||
import { CarInDelivery } from '../core/models/car-in-delivery.model';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class LogisticService {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
async getExpedicao() {
|
||||
const dataSource = new DataSource(typeOrmPgConfig);
|
||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -83,7 +78,6 @@ export class LogisticService {
|
||||
|
||||
const hoje = new Date();
|
||||
|
||||
// Criar uma nova data para amanhã
|
||||
let amanha = new Date(hoje);
|
||||
amanha.setDate(hoje.getDate() + 1);
|
||||
const amanhaString = amanha.toISOString().split('T')[0];
|
||||
@@ -104,7 +98,7 @@ export class LogisticService {
|
||||
}
|
||||
|
||||
async getDeliveries(placa: string) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
const dataSource = new DataSource(createOracleConfig(this.configService));
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -148,7 +142,7 @@ export class LogisticService {
|
||||
}
|
||||
|
||||
async getStatusCar(placa: string) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -172,7 +166,7 @@ export class LogisticService {
|
||||
}
|
||||
|
||||
async getEmployee() {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
const dataSource = new DataSource(createOracleConfig(this.configService));
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -195,7 +189,7 @@ export class LogisticService {
|
||||
|
||||
async createCarOut(data: CarOutDelivery) {
|
||||
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -205,13 +199,13 @@ export class LogisticService {
|
||||
const sqlSequence = `SELECT ESS_SAIDAVEICULO.NEXTVAL as "id" FROM DUAL`;
|
||||
const dataSequence = await queryRunner.query(sqlSequence);
|
||||
let i = 0;
|
||||
let helperId1: number = 0;
|
||||
let helperId2: number = 0;
|
||||
let helperId3: number = 0;
|
||||
let image1: string = '';
|
||||
let image2: string = '';
|
||||
let image3: string = '';
|
||||
let image4: string = '';
|
||||
let helperId1 = 0;
|
||||
let helperId2 = 0;
|
||||
let helperId3 = 0;
|
||||
const image1 = '';
|
||||
const image2 = '';
|
||||
const image3 = '';
|
||||
const image4 = '';
|
||||
|
||||
data.helpers.forEach(helper => {
|
||||
switch (i) {
|
||||
@@ -270,7 +264,7 @@ export class LogisticService {
|
||||
|
||||
async createCarIn(data: CarInDelivery) {
|
||||
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
@@ -290,11 +284,11 @@ export class LogisticService {
|
||||
throw new HttpException('Não foi localiza viagens em aberto para este veículo.', HttpStatus.BAD_REQUEST );
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
let image1: string = '';
|
||||
let image2: string = '';
|
||||
let image3: string = '';
|
||||
let image4: string = '';
|
||||
const i = 0;
|
||||
const image1 = '';
|
||||
const image2 = '';
|
||||
const image3 = '';
|
||||
const image4 = '';
|
||||
|
||||
for (let y = 0; y < data.invoices.length; y++) {
|
||||
const invoice = data.invoices[y];
|
||||
|
||||
78
src/main.ts
78
src/main.ts
@@ -1,27 +1,71 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import * as fs from 'fs';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { ResponseInterceptor } from './common/response.interceptor';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import helmet from 'helmet';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { join } from 'path';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
|
||||
// const privateKey = fs.readFileSync('cert/portal_juru.key', 'utf8');
|
||||
// const certificate = fs.readFileSync('cert/portal_juru.crt', 'utf8');
|
||||
// const httpsOptions = { key: privateKey, cert: certificate };
|
||||
// const app = await NestFactory.create(AppModule, {
|
||||
// httpsOptions,
|
||||
// });
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: [`'self'`],
|
||||
scriptSrc: [`'self'`, `'unsafe-inline'`, 'cdn.jsdelivr.net', 'cdnjs.cloudflare.com'],
|
||||
styleSrc: [`'self'`, `'unsafe-inline'`, 'cdnjs.cloudflare.com'],
|
||||
imgSrc: [`'self'`, 'data:'],
|
||||
connectSrc: [`'self'`],
|
||||
fontSrc: [`'self'`, 'cdnjs.cloudflare.com'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// const app = await NestFactory.create(AppModule);
|
||||
//Configurando o CORS
|
||||
app.enableCors({
|
||||
origin: '*', //'http://portal.jurunense.com', // Especifique a origem permitida ou use '*' para permitir todas
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', // Métodos HTTP permitidos
|
||||
credentials: true, // Permitir envio de cookies
|
||||
allowedHeaders: 'Content-Type, Accept', // Cabeçalhos permitidos
|
||||
// Configurar pasta de arquivos estáticos
|
||||
app.useStaticAssets(join(__dirname, '..', 'public'), {
|
||||
index: false,
|
||||
prefix: '/dashboard',
|
||||
});
|
||||
|
||||
await app.listen(3001);
|
||||
app.useGlobalInterceptors(new ResponseInterceptor());
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
forbidUnknownValues: true,
|
||||
disableErrorMessages: process.env.NODE_ENV === 'production',
|
||||
}),
|
||||
);
|
||||
|
||||
app.enableCors({
|
||||
origin: process.env.NODE_ENV === 'production'
|
||||
? ['https://seu-dominio.com', 'https://admin.seu-dominio.com']
|
||||
: '*',
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||
credentials: true,
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
|
||||
maxAge: 3600,
|
||||
});
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Portal Jurunense API')
|
||||
.setDescription('Documentação da API do Portal Jurunense')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('docs', app, document);
|
||||
|
||||
await app.listen(8066);
|
||||
|
||||
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
17
src/orders-payment/dto/create-invoice.dto.ts
Normal file
17
src/orders-payment/dto/create-invoice.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateInvoiceDto {
|
||||
@ApiProperty({
|
||||
description: 'ID do pedido',
|
||||
example: 12345,
|
||||
required: true,
|
||||
})
|
||||
orderId: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID do usuário',
|
||||
example: '001',
|
||||
required: true,
|
||||
})
|
||||
userId: number;
|
||||
}
|
||||
66
src/orders-payment/dto/create-payment.dto.ts
Normal file
66
src/orders-payment/dto/create-payment.dto.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreatePaymentDto {
|
||||
@ApiProperty({
|
||||
description: 'ID do pedido',
|
||||
example: 12345,
|
||||
required: true,
|
||||
})
|
||||
orderId: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Número do cartão',
|
||||
example: '**** **** **** 1234',
|
||||
required: true,
|
||||
})
|
||||
card: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Código de autorização',
|
||||
example: 'A12345',
|
||||
required: true,
|
||||
})
|
||||
auth: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'NSU da transação',
|
||||
example: '123456789',
|
||||
required: true,
|
||||
})
|
||||
nsu: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Número de parcelas',
|
||||
example: 3,
|
||||
required: true,
|
||||
})
|
||||
installments: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Valor do pagamento',
|
||||
example: 1000.00,
|
||||
required: true,
|
||||
})
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Nome da bandeira',
|
||||
example: 'VISA',
|
||||
required: true,
|
||||
})
|
||||
flagName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Tipo de pagamento',
|
||||
example: 'CREDITO',
|
||||
required: true,
|
||||
})
|
||||
paymentType: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID do usuário',
|
||||
example: '001',
|
||||
required: true,
|
||||
})
|
||||
userId: number;
|
||||
}
|
||||
91
src/orders-payment/dto/order.dto.ts
Normal file
91
src/orders-payment/dto/order.dto.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class OrderDto {
|
||||
@ApiProperty({
|
||||
description: 'Data de criação do pedido',
|
||||
example: '2024-04-02T10:00:00Z',
|
||||
})
|
||||
createDate: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID da loja',
|
||||
example: '001',
|
||||
})
|
||||
storeId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID do pedido',
|
||||
example: 12345,
|
||||
})
|
||||
orderId: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID do cliente',
|
||||
example: '12345',
|
||||
})
|
||||
customerId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Nome do cliente',
|
||||
example: 'João da Silva',
|
||||
})
|
||||
customerName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID do vendedor',
|
||||
example: '001',
|
||||
})
|
||||
sellerId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Nome do vendedor',
|
||||
example: 'Maria Santos',
|
||||
})
|
||||
sellerName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID da forma de pagamento',
|
||||
example: '001',
|
||||
})
|
||||
billingId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Nome da forma de pagamento',
|
||||
example: 'Cartão de Crédito',
|
||||
})
|
||||
billingName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID do plano de pagamento',
|
||||
example: '001',
|
||||
})
|
||||
planId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Nome do plano de pagamento',
|
||||
example: '3x sem juros',
|
||||
})
|
||||
planName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Valor total do pedido',
|
||||
example: 1000.00,
|
||||
})
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Número de parcelas',
|
||||
example: 3,
|
||||
})
|
||||
installments: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Valor total pago',
|
||||
example: 1000.00,
|
||||
})
|
||||
amountPaid: number;
|
||||
|
||||
constructor(partial: Partial<OrderDto>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
67
src/orders-payment/dto/payment.dto.ts
Normal file
67
src/orders-payment/dto/payment.dto.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class PaymentDto {
|
||||
@ApiProperty({
|
||||
description: 'ID do pedido',
|
||||
example: 12345,
|
||||
})
|
||||
orderId: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Data do pagamento',
|
||||
example: '2024-04-02T10:00:00Z',
|
||||
})
|
||||
payDate: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Número do cartão',
|
||||
example: '**** **** **** 1234',
|
||||
})
|
||||
card: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Número de parcelas',
|
||||
example: 3,
|
||||
})
|
||||
installments: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Nome da bandeira',
|
||||
example: 'VISA',
|
||||
})
|
||||
flagName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Tipo de pagamento',
|
||||
example: 'CREDITO',
|
||||
})
|
||||
type: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Valor do pagamento',
|
||||
example: 1000.00,
|
||||
})
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID do usuário',
|
||||
example: '001',
|
||||
})
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'NSU da transação',
|
||||
example: '123456789',
|
||||
})
|
||||
nsu: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Código de autorização',
|
||||
example: 'A12345',
|
||||
})
|
||||
auth: string;
|
||||
|
||||
constructor(partial: Partial<PaymentDto>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,74 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/controllers#controllers
|
||||
*/
|
||||
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
||||
import { OrdersPaymentService } from './orders-payment.service';
|
||||
import { OrderDto } from './dto/order.dto';
|
||||
import { PaymentDto } from './dto/payment.dto';
|
||||
import { CreatePaymentDto } from './dto/create-payment.dto';
|
||||
import { CreateInvoiceDto } from './dto/create-invoice.dto';
|
||||
|
||||
@ApiTags('Orders Payment')
|
||||
@Controller('api/v1/orders-payment')
|
||||
export class OrdersPaymentController {
|
||||
|
||||
constructor(private readonly orderPaymentService: OrdersPaymentService){}
|
||||
|
||||
@Get('orders/:id')
|
||||
findOrders(@Param('id') storeId: string) {
|
||||
@ApiOperation({ summary: 'Lista todos os pedidos de uma loja' })
|
||||
@ApiParam({ name: 'id', description: 'ID da loja' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Lista de pedidos retornada com sucesso',
|
||||
type: [OrderDto]
|
||||
})
|
||||
async findOrders(@Param('id') storeId: string): Promise<OrderDto[]> {
|
||||
return this.orderPaymentService.findOrders(storeId, 0);
|
||||
}
|
||||
|
||||
@Get('orders/:id/:orderId')
|
||||
findOrder(@Param('id') storeId: string,
|
||||
@Param('orderId') orderId: number) {
|
||||
return this.orderPaymentService.findOrders(storeId, orderId);
|
||||
@ApiOperation({ summary: 'Busca um pedido específico' })
|
||||
@ApiParam({ name: 'id', description: 'ID da loja' })
|
||||
@ApiParam({ name: 'orderId', description: 'ID do pedido' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Pedido retornado com sucesso',
|
||||
type: OrderDto
|
||||
})
|
||||
async findOrder(
|
||||
@Param('id') storeId: string,
|
||||
@Param('orderId') orderId: number,
|
||||
): Promise<OrderDto> {
|
||||
const orders = await this.orderPaymentService.findOrders(storeId, orderId);
|
||||
return orders[0];
|
||||
}
|
||||
|
||||
@Get('payments/:id')
|
||||
findPayments(@Param('id') orderId: number) {
|
||||
@ApiOperation({ summary: 'Lista todos os pagamentos de um pedido' })
|
||||
@ApiParam({ name: 'id', description: 'ID do pedido' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Lista de pagamentos retornada com sucesso',
|
||||
type: [PaymentDto]
|
||||
})
|
||||
async findPayments(@Param('id') orderId: number): Promise<PaymentDto[]> {
|
||||
return this.orderPaymentService.findPayments(orderId);
|
||||
}
|
||||
@Post('payments/create')
|
||||
createPayment(@Body() data: any) {
|
||||
@ApiOperation({ summary: 'Cria um novo pagamento' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Pagamento criado com sucesso'
|
||||
})
|
||||
async createPayment(@Body() data: CreatePaymentDto): Promise<void> {
|
||||
return this.orderPaymentService.createPayment(data);
|
||||
}
|
||||
|
||||
@Post('invoice/create')
|
||||
createInvoice(@Body() data: any) {
|
||||
@ApiOperation({ summary: 'Cria uma nova fatura' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Fatura criada com sucesso'
|
||||
})
|
||||
async createInvoice(@Body() data: CreateInvoiceDto): Promise<void> {
|
||||
return this.orderPaymentService.createInvoice(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,16 @@ https://docs.nestjs.com/modules
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OrdersPaymentController } from './orders-payment.controller';
|
||||
import { OrdersPaymentService } from './orders-payment.service';
|
||||
import { DatabaseModule } from '../core/database/database.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [
|
||||
OrdersPaymentController,],
|
||||
providers: [
|
||||
OrdersPaymentService,],
|
||||
imports: [
|
||||
ConfigModule,
|
||||
DatabaseModule,
|
||||
],
|
||||
controllers: [OrdersPaymentController],
|
||||
providers: [OrdersPaymentService],
|
||||
exports: [OrdersPaymentService],
|
||||
})
|
||||
export class OrdersPaymentModule { }
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/providers#services
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { typeOrmConfig } from 'src/core/configs/typeorm.config';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DATA_SOURCE } from '../core/constants';
|
||||
import { OrderDto } from './dto/order.dto';
|
||||
import { PaymentDto } from './dto/payment.dto';
|
||||
import { CreatePaymentDto } from './dto/create-payment.dto';
|
||||
import { CreateInvoiceDto } from './dto/create-invoice.dto';
|
||||
|
||||
@Injectable()
|
||||
export class OrdersPaymentService {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
@Inject(DATA_SOURCE) private readonly dataSource: DataSource
|
||||
) {}
|
||||
|
||||
async findOrders(storeId: string, orderId: number) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
async findOrders(storeId: string, orderId: number): Promise<OrderDto[]> {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
try {
|
||||
|
||||
const sql = `SELECT PCPEDC.DATA as "createDate"
|
||||
,PCPEDC.CODFILIAL as "storeId"
|
||||
,PCPEDC.NUMPED as "orderId"
|
||||
@@ -44,26 +43,21 @@ export class OrdersPaymentService {
|
||||
AND PCPEDC.DATA >= TRUNC(SYSDATE) - 5
|
||||
AND PCPEDC.CODFILIAL = ${storeId} `;
|
||||
let sqlWhere = '';
|
||||
if ( orderId > 0) {
|
||||
if (orderId > 0) {
|
||||
sqlWhere += ` AND PCPEDC.NUMPED = ${orderId}`;
|
||||
}
|
||||
|
||||
const orders = await queryRunner.manager.query(sql + sqlWhere);
|
||||
|
||||
return orders;
|
||||
return orders.map(order => new OrderDto(order));
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async findPayments(orderId: number) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
async findPayments(orderId: number): Promise<PaymentDto[]> {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
try {
|
||||
|
||||
const sql = `SELECT
|
||||
ESTPAGAMENTO.NUMORCA as "orderId"
|
||||
,ESTPAGAMENTO.DTPAGAMENTO as "payDate"
|
||||
@@ -79,46 +73,35 @@ export class OrdersPaymentService {
|
||||
WHERE ESTPAGAMENTO.NUMORCA = ${orderId}`;
|
||||
|
||||
const payments = await queryRunner.manager.query(sql);
|
||||
|
||||
console.log(JSON.stringify(payments));
|
||||
|
||||
return payments;
|
||||
return payments.map(payment => new PaymentDto(payment));
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async createPayment(payment: any) {
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
async createPayment(payment: CreatePaymentDto): Promise<void> {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
const sql = `INSERT INTO ESTPAGAMENTO ( NUMORCA, DTPAGAMENTO, CARTAO, CODAUTORIZACAO, CODRESPOSTA, DTREQUISICAO, DTSERVIDOR, IDTRANSACAO,
|
||||
NSU, PARCELAS, VALOR, NOMEBANDEIRA, FORMAPAGTO, DTPROCESSAMENTO, CODFUNC )
|
||||
VALUES ( ${payment.orderId}, TRUNC(SYSDATE), '${payment.card}', '${payment.auth}', '00', SYSDATE, SYSDATE, NULL /*'${payment.transcationId}'*/,
|
||||
VALUES ( ${payment.orderId}, TRUNC(SYSDATE), '${payment.card}', '${payment.auth}', '00', SYSDATE, SYSDATE, NULL,
|
||||
'${payment.nsu}', ${payment.installments}, ${payment.amount}, '${payment.flagName}',
|
||||
'${payment.paymentType}', SYSDATE, ${payment.userId} ) `;
|
||||
|
||||
await queryRunner.manager.query(sql);
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
} catch ( error ) {
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
console.log(error);
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async createInvoice(data: any) {
|
||||
|
||||
const dataSource = new DataSource(typeOrmConfig);
|
||||
await dataSource.initialize();
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
async createInvoice(data: CreateInvoiceDto): Promise<void> {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
try {
|
||||
@@ -127,15 +110,11 @@ export class OrdersPaymentService {
|
||||
END;`;
|
||||
await queryRunner.manager.query(sql);
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
} catch ( error ) {
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
console.log(error);
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
await dataSource.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
134
src/orders/application/orders.service.ts
Normal file
134
src/orders/application/orders.service.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { FindOrdersDto } from '../dto/find-orders.dto';
|
||||
import { InvoiceDto } from '../dto/find-invoice.dto';
|
||||
import { CutItemDto } from '../dto/CutItemDto';
|
||||
import { OrdersRepository } from '../repositories/orders.repository';
|
||||
import { OrderItemDto } from '../dto/OrderItemDto';
|
||||
import { IRedisClient } from 'src/core/configs/cache/IRedisClient';
|
||||
import { RedisClientToken } from 'src/core/configs/cache/redis-client.adapter.provider';
|
||||
import { getOrSetCache } from 'src/shared/cache.util';
|
||||
import { createHash } from 'crypto';
|
||||
import { OrderDeliveryDto } from '../dto/OrderDeliveryDto';
|
||||
import { OrderTransferDto } from '../dto/OrderTransferDto';
|
||||
import { OrderStatusDto } from '../dto/OrderStatusDto';
|
||||
import { InvoiceCheckDto } from '../dto/invoice-check.dto';
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
private readonly TTL_ORDERS = 60 * 10; // 10 minutos
|
||||
private readonly TTL_INVOICE = 60 * 60; // 1 hora
|
||||
private readonly TTL_ITENS = 60 * 10; // 10 minutos
|
||||
|
||||
constructor(
|
||||
private readonly ordersRepository: OrdersRepository,
|
||||
@Inject(RedisClientToken) private readonly redisClient: IRedisClient,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Buscar pedidos com cache baseado nos filtros
|
||||
*/
|
||||
async findOrders(query: FindOrdersDto) {
|
||||
const key = `orders:query:${this.hashObject(query)}`;
|
||||
|
||||
return getOrSetCache(
|
||||
this.redisClient,
|
||||
key,
|
||||
this.TTL_ORDERS,
|
||||
() => this.ordersRepository.findOrders(query),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar nota fiscal por chave NFe com cache
|
||||
*/
|
||||
async findInvoice(chavenfe: string): Promise<InvoiceDto> {
|
||||
const key = `orders:invoice:${chavenfe}`;
|
||||
|
||||
return getOrSetCache(this.redisClient, key, this.TTL_INVOICE, async () => {
|
||||
const invoiceData = await this.ordersRepository.findInvoice(chavenfe);
|
||||
|
||||
const invoice: InvoiceDto = {
|
||||
storeId: invoiceData.storeId,
|
||||
invoiceDate: new Date(invoiceData.invoiceDate),
|
||||
orderId: invoiceData.orderId,
|
||||
invoiceId: invoiceData.invoiceId,
|
||||
transactionId: invoiceData.transactionId,
|
||||
customerId: invoiceData.customerId,
|
||||
customer: invoiceData.customer,
|
||||
sellerId: invoiceData.sellerId,
|
||||
sellerName: invoiceData.sellerName,
|
||||
itensQt: invoiceData.itensQt,
|
||||
itens: invoiceData.itens,
|
||||
};
|
||||
|
||||
return invoice;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar itens de pedido com cache
|
||||
*/
|
||||
async getItens(orderId: string): Promise<OrderItemDto[]> {
|
||||
const key = `orders:itens:${orderId}`;
|
||||
|
||||
return getOrSetCache(this.redisClient, key, this.TTL_ITENS, async () => {
|
||||
const itens = await this.ordersRepository.getItens(orderId);
|
||||
|
||||
return itens.map(item => ({
|
||||
productId: Number(item.productId),
|
||||
description: item.description,
|
||||
pacth: item.pacth,
|
||||
color: item.color,
|
||||
stockId: Number(item.stockId),
|
||||
quantity: Number(item.quantity),
|
||||
salePrice: Number(item.salePrice),
|
||||
deliveryType: item.deliveryType,
|
||||
total: Number(item.total),
|
||||
weight: Number(item.weight),
|
||||
department: item.department,
|
||||
brand: item.brand,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async getCutItens(orderId: string): Promise<CutItemDto[]> {
|
||||
const itens = await this.ordersRepository.getCutItens(orderId);
|
||||
|
||||
return itens.map(item => ({
|
||||
productId: Number(item.productId),
|
||||
description: item.description,
|
||||
pacth: item.pacth,
|
||||
stockId: Number(item.stockId),
|
||||
saleQuantity: Number(item.saleQuantity),
|
||||
cutQuantity: Number(item.cutQuantity),
|
||||
separedQuantity: Number(item.separedQuantity),
|
||||
}));
|
||||
}
|
||||
|
||||
async getOrderDelivery(orderId: string): Promise<OrderDeliveryDto> {
|
||||
return this.ordersRepository.getOrderDelivery(orderId);
|
||||
}
|
||||
|
||||
async getTransfer(orderId: number): Promise<OrderTransferDto[] | null> {
|
||||
return this.ordersRepository.getTransfer(orderId);
|
||||
}
|
||||
|
||||
async getStatusOrder(orderId: number): Promise<OrderStatusDto[] | null> {
|
||||
return this.ordersRepository.getStatusOrder(orderId);
|
||||
}
|
||||
/**
|
||||
* Utilitário para gerar hash MD5 de objetos
|
||||
*/
|
||||
private hashObject(obj: any): string {
|
||||
const str = JSON.stringify(obj, Object.keys(obj).sort());
|
||||
return createHash('md5').update(str).digest('hex');
|
||||
}
|
||||
|
||||
|
||||
async createInvoiceCheck(invoice: InvoiceCheckDto): Promise<{ message: string }> {
|
||||
return this.ordersRepository.createInvoiceCheck(invoice);
|
||||
}
|
||||
|
||||
}
|
||||
141
src/orders/controllers/orders.controller.ts
Normal file
141
src/orders/controllers/orders.controller.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UsePipes,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
ValidationPipe,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ResponseInterceptor } from '../../common/response.interceptor';
|
||||
import { OrdersService } from '../application/orders.service';
|
||||
import { FindOrdersDto } from '../dto/find-orders.dto';
|
||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||
import { InvoiceDto } from '../dto/find-invoice.dto';
|
||||
import { OrderItemDto } from "../dto/OrderItemDto";
|
||||
import { CutItemDto } from '../dto/CutItemDto';
|
||||
import { OrderDeliveryDto } from '../dto/OrderDeliveryDto';
|
||||
import { OrderTransferDto } from '../dto/OrderTransferDto';
|
||||
import { OrderStatusDto } from '../dto/OrderStatusDto';
|
||||
import { InvoiceCheckDto } from '../dto/invoice-check.dto';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ApiTags('Orders')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseInterceptors(ResponseInterceptor)
|
||||
@Controller('api/v1/orders')
|
||||
export class OrdersController {
|
||||
constructor(private readonly ordersService: OrdersService) {}
|
||||
|
||||
@Get('find')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
findOrders(@Query() query: FindOrdersDto) {
|
||||
return this.ordersService.findOrders(query);
|
||||
}
|
||||
|
||||
@Get('invoice/:chavenfe')
|
||||
@ApiOperation({ summary: 'Busca NF pela chave' })
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async getInvoice(@Param('chavenfe') chavenfe: string): Promise<InvoiceDto> {
|
||||
try {
|
||||
return await this.ordersService.findInvoice(chavenfe);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Erro ao buscar nota fiscal',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@Get('itens/:orderId')
|
||||
@ApiOperation({ summary: 'Busca PELO numero do pedido' })
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async getItens(@Param('orderId') orderId: string): Promise<OrderItemDto[]> {
|
||||
try {
|
||||
return await this.ordersService.getItens(orderId);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Erro ao buscar itens do pedido',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@Get('cut-itens/:orderId')
|
||||
@ApiOperation({ summary: 'Busca itens cortados do pedido' })
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async getCutItens(@Param('orderId') orderId: string): Promise<CutItemDto[]> {
|
||||
try {
|
||||
return await this.ordersService.getCutItens(orderId);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Erro ao buscar itens cortados',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('delivery/:orderId')
|
||||
@ApiOperation({ summary: 'Busca dados de entrega do pedido' })
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async getOrderDelivery(@Param('orderId') orderId: string): Promise<OrderDeliveryDto | null> {
|
||||
try {
|
||||
return await this.ordersService.getOrderDelivery(orderId);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Erro ao buscar dados de entrega',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('transfer/:orderId')
|
||||
@ApiOperation({ summary: 'Consulta pedidos de transferência' })
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async getTransfer(@Param('orderId') orderId: number): Promise<OrderTransferDto[] | null> {
|
||||
try {
|
||||
return await this.ordersService.getTransfer(orderId);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Erro ao buscar transferências do pedido',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('status/:orderId')
|
||||
@ApiOperation({ summary: 'Consulta status do pedido' })
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async getStatusOrder(@Param('orderId') orderId: number): Promise<OrderStatusDto[] | null> {
|
||||
try {
|
||||
return await this.ordersService.getStatusOrder(orderId);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Erro ao buscar status do pedido',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('invoice/check')
|
||||
@ApiOperation({ summary: 'Cria conferência de nota fiscal' })
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async createInvoiceCheck(@Body() invoice: InvoiceCheckDto): Promise<{ message: string }> {
|
||||
try {
|
||||
return await this.ordersService.createInvoiceCheck(invoice);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Erro ao salvar conferência',
|
||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/orders/dto/CutItemDto.ts
Normal file
10
src/orders/dto/CutItemDto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export class CutItemDto {
|
||||
productId: number;
|
||||
description: string;
|
||||
pacth: string;
|
||||
stockId: number;
|
||||
saleQuantity: number;
|
||||
cutQuantity: number;
|
||||
separedQuantity: number;
|
||||
}
|
||||
|
||||
28
src/orders/dto/OrderDeliveryDto.ts
Normal file
28
src/orders/dto/OrderDeliveryDto.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export class OrderDeliveryDto {
|
||||
placeId: number;
|
||||
placeName: string;
|
||||
street: string;
|
||||
addressNumber: string;
|
||||
bairro: string;
|
||||
city: string;
|
||||
state: string;
|
||||
addressComplement: string;
|
||||
cep: string;
|
||||
commentOrder1: string;
|
||||
commentOrder2: string;
|
||||
commentDelivery1: string;
|
||||
commentDelivery2: string;
|
||||
commentDelivery3: string;
|
||||
commentDelivery4: string;
|
||||
shippimentId: number;
|
||||
shippimentDate: Date;
|
||||
shippimentComment: string;
|
||||
place: string;
|
||||
driver: string;
|
||||
car: string;
|
||||
closeDate: Date;
|
||||
separatorName: string;
|
||||
confName: string;
|
||||
releaseDate: Date;
|
||||
}
|
||||
|
||||
14
src/orders/dto/OrderItemDto.ts
Normal file
14
src/orders/dto/OrderItemDto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export class OrderItemDto {
|
||||
productId: number;
|
||||
description: string;
|
||||
pacth: string;
|
||||
color: string;
|
||||
stockId: number;
|
||||
quantity: number;
|
||||
salePrice: number;
|
||||
deliveryType: string;
|
||||
total: number;
|
||||
weight: number;
|
||||
department: string;
|
||||
brand: string;
|
||||
}
|
||||
8
src/orders/dto/OrderStatusDto.ts
Normal file
8
src/orders/dto/OrderStatusDto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export class OrderStatusDto {
|
||||
orderId: number;
|
||||
status: string;
|
||||
statusDate: Date;
|
||||
userName: string;
|
||||
comments: string | null;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user