Merge pull request #3 from JurunenseDesenvolvimento/dev

Dev
This commit is contained in:
Alessandro Gonçalves
2025-05-15 10:24:58 -03:00
committed by GitHub
121 changed files with 10586 additions and 5998 deletions

38
.env Normal file
View 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
View 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
View File

@@ -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
View File

@@ -0,0 +1,3 @@
{
"compile-hero.disable-compile-files-on-did-save-code": false
}

300
API.md Normal file
View 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
View 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
View File

@@ -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>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](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
View 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

View 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.

View 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
View 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
```

View 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
![Dashboard Principal](https://via.placeholder.com/800x400?text=Portal+Jurunense+Main+Dashboard)
**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
![Dashboard de Banco de Dados](
**Painéis Incluídos:**
- Uso do pool de conexões
- Tempo de resposta de queries
- Queries em execução
- Erros de banco de dados
- Saúde das conexões
#### Dashboard de Negócio
![Dashboard de Negócio]
**Painéis Incluídos:**
- Pedidos por hora/dia
- Taxa de conversão de pagamentos
- Erros de processamento
- Tempo de resposta de APIs externas
- Alertas ativos
### 3. Sistema de Alertas
#### Alertas Implementados
- **Disponibilidade**: Serviço fora do ar
- **Performance**: Tempo de resposta elevado
- **Recursos**: Uso elevado de memória, CPU ou disco
- **Banco de Dados**: Pool de conexões quase esgotado
- **Negócio**: Erros de integração com serviços externos
#### Canais de Notificação
- **Email**: Para alertas não críticos e relatórios diários
- **Slack**: Para alertas em tempo real, com cores baseadas na severidade
- **Microsoft Teams**: Alternativa para organizações que usam Teams
- **SMS/Chamada** (opcional): 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.

View 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']

View 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

View File

@@ -0,0 +1,9 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false

View 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']

View 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."

View File

@@ -1,4 +1,7 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src"
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

8904
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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
View File

@@ -0,0 +1,6 @@
export interface ILogger {
log(message: string): void;
warn(message: string): void;
error(message: string, trace?: string): void;
}

View 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;
}
}

View 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
View 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 {}

View File

@@ -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!');
});
});
});

View File

@@ -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();
}
}

View File

@@ -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');
}
}

View File

@@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -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!' };
}
}

View File

@@ -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 {}

View File

@@ -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,
};
}
}

View File

@@ -0,0 +1,7 @@
export class AuthenticateUserCommand {
constructor(
public readonly username: string,
public readonly password: string,
) {}
}

View 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);
}
}

View 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;
}

View 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;
}

View 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
View 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
View 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);
}
}

View File

@@ -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}`;
}
}

View 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;
}
}

View 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;
}
}

View 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}`);
}
}

View 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 };
}
}

View File

@@ -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 {}

View File

@@ -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();
});
});

View File

@@ -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);
}
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 changePassword(user: { id: number; password: string; newPassword: string }) {
return this.changePasswordService.execute(user.id, user.password, user.newPassword);
}
}
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();
}
}
}

View 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)));
}
}

View 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;
}
}

View 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);
}),
);
}
}

View 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';
},
},
});
};
}

View 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
View 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&lt;T&gt;(key: string): Promise&lt;T | null&gt;;
set&lt;T&gt;(key: string, value: T, ttlSeconds?: number): Promise&lt;void&gt;;
del(key: string): Promise&lt;void&gt;;
}</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) =&gt; {
const redis = new Redis({
host: configService.get('REDIS_HOST', '10.1.1.109'),
port: configService.get('REDIS_PORT', 6379),
});
redis.on('error', (err) =&gt; {
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&lt;T&gt;(key: string): Promise&lt;T | null&gt; {
const data = await this.redis.get(key);
return data ? JSON.parse(data) : null;
}
async set&lt;T&gt;(key: string, value: T, ttlSeconds = 300): Promise&lt;void&gt; {
await this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
}
async del(key: string): Promise&lt;void&gt; {
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&lt;T&gt;('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>

View File

@@ -0,0 +1,8 @@
import { RedisClientAdapter } from './redis-client.adapter';
export const RedisClientToken = 'RedisClientInterface';
export const RedisClientAdapterProvider = {
provide: RedisClientToken,
useClass: RedisClientAdapter,
};

View 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
View 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 {}

View 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],
};

View File

@@ -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,
},
}));

View 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;
}

View 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
View File

@@ -0,0 +1 @@
export const DATA_SOURCE = 'DATA_SOURCE';

View 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 {}

View File

@@ -21,5 +21,4 @@ export class Invoice {
remnant: string;
observationRemnant: string;
imagesRemnant: string[];
}

View File

@@ -9,10 +9,8 @@ export class CarOutDelivery {
vehicleCode: number; // Código do veículo
}
export class Helper {
id: number;
name: string;
phone: string;
}

View File

@@ -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,
};
}
}

View File

@@ -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 { }

View File

@@ -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: "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", // process.env.AWS_SECRET_ACCESS_KEY,
});
}
}

View File

@@ -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();
}
}

View File

@@ -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 {}

View 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));
}
}

View File

@@ -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);
}
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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}`);
}
}
}

View 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(),
]);
}
}

View 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 {}

View 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);
}
}

View 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);
}
}
}

View 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);
}
}

View 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],
}),
];

View 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');
}
}

View File

@@ -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';
@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);
}
}

View File

@@ -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];

View File

@@ -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();

View 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;
}

View 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;
}

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 { }

View File

@@ -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"
@@ -49,21 +48,16 @@ export class OrdersPaymentService {
}
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) {
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) {
await queryRunner.rollbackTransaction();
console.log(error);
throw error;
} finally {
await queryRunner.release();
await dataSource.destroy();
}
}
}

View 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);
}
}

View 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
);
}
}
}

View File

@@ -0,0 +1,10 @@
export class CutItemDto {
productId: number;
description: string;
pacth: string;
stockId: number;
saleQuantity: number;
cutQuantity: number;
separedQuantity: number;
}

View 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;
}

View 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;
}

View 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