Compare commits

...

11 Commits

Author SHA1 Message Date
e2e5bf5b3f refactor: desativa conexao com postgres
Comenta integracao com WMS/leadtime e remove DataSource postgres do bootstrap e repositories.
2026-01-28 09:32:03 -03:00
83a1fd78be feat: adiciona endpoint placa-8122 e remove newrelic/pm2
Simplifica start:prod e ajusta consultas de ofertas para 10x (codplpagmax=42), com melhorias de sanitização e imports.
2026-01-28 09:19:11 -03:00
JurTI-BR
b13e2775b4 feat: adiciona configuração do PM2 para gerenciamento de processos 2025-12-17 11:44:20 -03:00
JuruSysadmin
8cfcaf3910 feat: adiciona endpoint oferta-8026 para buscar ofertas promocionais 2025-11-24 16:57:46 -03:00
JuruSysadmin
17bec31bf8 refactor: remove LogisticController, LogisticService and OrdersModule from AppModule 2025-11-21 17:17:04 -03:00
JuruSysadmin
233734fdea fix: corrige erros do ESLint e configura variáveis globais
- Adiciona variáveis globais do Node.js (process, console, __dirname, require, module, exports)
- Adiciona variáveis globais do Jest (describe, it, beforeEach, fail, etc.)
- Configura ESLint para arquivos JavaScript de configuração
- Remove diretivas eslint-disable não utilizadas
- Corrige variáveis não usadas prefixando com _
- Ajusta regras do ESLint para ignorar variáveis que começam com _
- Formata código com Prettier
2025-11-21 17:05:07 -03:00
JuruSysadmin
d5286fe91a chore: atualiza app.module.ts 2025-11-21 16:52:20 -03:00
JuruSysadmin
32da5b1466 fix: adiciona variáveis globais do Jest no ESLint config 2025-11-21 16:46:18 -03:00
Joelson
0760ddf631 Potential fix for pull request finding 'Unused variable, import, function or class'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2025-11-14 17:38:10 -03:00
joelson brito
86cbe8e431 feat: adiciona sistema de versionamento e releases automáticas
- Implementa versionamento semântico para imagens Docker
- Adiciona job de release automática no GitHub Actions
- Releases criadas apenas para tags na branch main
- Adiciona documentação de versionamento em docs/VERSIONAMENTO.md
- Suporte a tags semânticas (v0.1.0, v0.5.0, etc)
- Versionamento baseado em package.json e tags Git
2025-11-14 17:24:38 -03:00
joelson brito
beecabcbfd fix: corrige .dockerignore e Dockerfile para build funcionar
- Remove tsconfig*.json do .dockerignore (necessário para build)
- Remove nest-cli.json do .dockerignore (necessário para build)
- Corrige LD_LIBRARY_PATH no Dockerfile
2025-11-14 17:09:07 -03:00
70 changed files with 1866 additions and 1230 deletions

View File

@@ -26,8 +26,7 @@ jest.setup.js
eslint.config.js
.prettierrc
.prettierignore
tsconfig*.json
nest-cli.json
monitoring
docs

2
.env
View File

@@ -31,7 +31,7 @@ THROTTLE_LIMIT=10
NODE_ENV=development
ORACLE_CLIENT_LIB_DIR=C:\\instantclient_23_9
ORACLE_CLIENT_LIB_DIR=C:\\instantclient_23_0

View File

@@ -3,6 +3,8 @@ name: CI
on:
push:
branches: [ main, master, develop, homologacao ]
tags:
- 'v*'
pull_request:
branches: [ main, master, develop, homologacao ]
@@ -148,6 +150,13 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get package version
id: package-version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Package version: $VERSION"
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
@@ -156,8 +165,12 @@ jobs:
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.ref_type == 'branch' }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
@@ -169,3 +182,73 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
release:
name: Create Release
runs-on: ubuntu-latest
needs: [build, docker-build]
if: startsWith(github.ref, 'refs/tags/v') && github.ref_type == 'tag'
permissions:
contents: write
env:
REGISTRY: ghcr.io
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Check if tag is on main branch
id: check-branch
run: |
git fetch origin main:main 2>/dev/null || true
if git branch -r --contains ${{ github.ref_name }} | grep -q 'origin/main'; then
echo "on_main=true" >> $GITHUB_OUTPUT
echo "Tag is on main branch"
else
echo "on_main=false" >> $GITHUB_OUTPUT
echo "Tag is not on main branch, skipping release"
fi
- name: Get version from tag
id: version
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$TAG_VERSION" >> $GITHUB_OUTPUT
echo "Tag version: $TAG_VERSION"
- name: Generate changelog
id: changelog
run: |
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
CHANGELOG=$(git log --pretty=format:"- %s (%h)" HEAD)
else
CHANGELOG=$(git log --pretty=format:"- %s (%h)" ${PREVIOUS_TAG}..HEAD)
fi
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
if: steps.check-branch.outputs.on_main == 'true'
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.ref_name }}
name: Release ${{ steps.version.outputs.version }}
body: |
## Versão ${{ steps.version.outputs.version }}
### Mudanças
${{ steps.changelog.outputs.changelog }}
### Docker Image
```bash
docker pull ${{ env.REGISTRY }}/${{ github.repository }}:${{ steps.version.outputs.version }}
docker pull ${{ env.REGISTRY }}/${{ github.repository }}:latest
```
draft: false
prerelease: false

View File

@@ -41,7 +41,7 @@ RUN curl -fSL --cookie-jar /tmp/cookies.txt --retry 3 \
rm -f /tmp/oracle-instantclient-basiclite.rpm /tmp/cookies.txt && \
dnf clean all
ENV LD_LIBRARY_PATH=/usr/lib/oracle/21/client64/lib:$LD_LIBRARY_PATH
ENV LD_LIBRARY_PATH=/usr/lib/oracle/21/client64/lib
ENV TZ=America/Sao_Paulo
RUN groupadd -r node && useradd -r -g node node

127
docs/VERSIONAMENTO.md Normal file
View File

@@ -0,0 +1,127 @@
# Sistema de Versionamento
Este documento descreve o sistema de versionamento utilizado no projeto, incluindo como as imagens Docker são versionadas e como criar releases no GitHub.
## Visão Geral
O projeto utiliza versionamento semântico baseado no arquivo `package.json` e tags Git para gerenciar versões de imagens Docker e releases no GitHub Container Registry (GHCR).
## Versionamento de Imagens Docker
As imagens Docker são automaticamente versionadas durante o processo de CI/CD baseado no contexto do push:
### Branches
- **Branch `homologacao`**: `ghcr.io/usuario/repo:homologacao`
- **Branch `main`**:
- `ghcr.io/usuario/repo:main`
- `ghcr.io/usuario/repo:latest`
- `ghcr.io/usuario/repo:{versao-do-package.json}`
### Tags Git
Quando uma tag é criada (formato `v*`, exemplo: `v0.1.0`), as seguintes tags são geradas:
- `ghcr.io/usuario/repo:0.1.0` (versão completa)
- `ghcr.io/usuario/repo:0.1` (major.minor)
- `ghcr.io/usuario/repo:0` (major)
### Commits
Cada commit gera uma tag com o SHA do commit:
- `ghcr.io/usuario/repo:{branch}-{sha}`
## Releases no GitHub
Releases são criadas automaticamente quando uma tag Git no formato `v*` é enviada para a branch `main`. O processo inclui:
1. Verificação se a tag está na branch `main`
2. Geração automática de changelog baseado nos commits desde a última tag
3. Criação da release no GitHub com:
- Nome da versão
- Changelog completo
- Instruções para download da imagem Docker
## Como Criar uma Nova Versão
### Para Homologação
1. Atualize a versão no `package.json`:
```bash
npm version patch # Incrementa patch: 0.0.1 -> 0.0.2
npm version minor # Incrementa minor: 0.0.1 -> 0.1.0
npm version major # Incrementa major: 0.0.1 -> 1.0.0
```
2. Faça commit e push:
```bash
git add package.json package-lock.json
git commit -m "chore: bump version to 0.1.0"
git push origin homologacao
```
A imagem Docker será automaticamente buildada e publicada com a tag correspondente à branch.
### Para Produção (Release)
1. Faça merge da branch `homologacao` para `main`:
```bash
git checkout main
git merge homologacao
```
2. Crie uma tag e faça push:
```bash
git tag v0.1.0
git push origin main --tags
```
Este processo irá:
- Buildar a imagem Docker com as tags de versão semântica
- Criar automaticamente uma release no GitHub
- Publicar a imagem no GitHub Container Registry
## Verificar Versão Atual
A versão atual do projeto está definida no arquivo `package.json`:
```json
{
"version": "0.0.1"
}
```
## Estrutura de Tags Docker
As imagens Docker seguem o seguinte padrão de nomenclatura:
- **Desenvolvimento**: `{registry}/{repo}:{branch-name}`
- **Versão específica**: `{registry}/{repo}:{version}` (ex: `0.1.0`)
- **Versão parcial**: `{registry}/{repo}:{major}.{minor}` (ex: `0.1`)
- **Major version**: `{registry}/{repo}:{major}` (ex: `0`)
- **Latest**: `{registry}/{repo}:latest` (apenas branch main)
- **Commit SHA**: `{registry}/{repo}:{branch}-{sha}`
## Workflow de CI/CD
O workflow de CI/CD está configurado no arquivo `.github/workflows/ci.yml` e executa os seguintes jobs:
1. **Lint**: Verificação de código e formatação
2. **Build**: Compilação do projeto TypeScript
3. **Test**: Execução de testes unitários
4. **Test E2E**: Execução de testes end-to-end (apenas PRs e main)
5. **Docker Build**: Build e push da imagem Docker
6. **Release**: Criação de release no GitHub (apenas tags na main)
## Notas Importantes
- Tags devem seguir o formato semântico: `v{major}.{minor}.{patch}` (ex: `v1.0.0`)
- Releases são criadas apenas para tags na branch `main`
- A versão no `package.json` é usada para versionar imagens em branches
- Tags Git são usadas para versionar imagens em releases de produção

View File

@@ -18,6 +18,22 @@ module.exports = [
globals: {
node: true,
jest: true,
process: 'readonly',
console: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
require: 'readonly',
module: 'readonly',
exports: 'readonly',
describe: 'readonly',
it: 'readonly',
test: 'readonly',
expect: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
fail: 'readonly',
},
},
plugins: {
@@ -30,9 +46,65 @@ module.exports = [
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-require-imports': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'no-useless-escape': 'warn',
'no-import-assign': 'off',
'prettier/prettier': 'error',
},
},
{
files: ['**/*.spec.ts', '**/__tests__/**/*.ts', '**/test/**/*.ts'],
languageOptions: {
globals: {
describe: 'readonly',
it: 'readonly',
test: 'readonly',
expect: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
jest: 'readonly',
fail: 'readonly',
require: 'readonly',
},
},
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
},
{
files: ['**/*.js'],
languageOptions: {
globals: {
node: true,
jest: true,
process: 'readonly',
console: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
require: 'readonly',
module: 'readonly',
exports: 'readonly',
global: 'readonly',
Buffer: 'readonly',
},
ecmaVersion: 'latest',
sourceType: 'commonjs',
},
rules: {
'no-undef': 'off',
'@typescript-eslint/no-require-imports': 'off',
},
},
{
ignores: ['dist/**', 'node_modules/**', 'coverage/**'],
},

553
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,12 @@ 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';
import { AuthModule } from './auth/auth/auth.module';
import { DataConsultModule } from './data-consult/data-consult.module';
import { OrdersModule } from './orders/modules/orders.module';
import { HttpModule } from '@nestjs/axios';
import { DebModule } from './orders/modules/deb.module';
import { LogisticController } from './logistic/logistic.controller';
import { LogisticService } from './logistic/logistic.service';
import jwtConfig from './auth/jwt.config';
import { UsersModule } from './auth/users/users.module';
import { ProductsModule } from './products/products.module';
@@ -30,11 +26,12 @@ import { PartnersModule } from './partners/partners.module';
inject: [ConfigService],
useFactory: createOracleConfig,
}),
TypeOrmModule.forRootAsync({
name: 'postgres',
inject: [ConfigService],
useFactory: createPostgresConfig,
}),
// Postgres desativado: conexao removida e trechos de uso comentados no codigo.
// TypeOrmModule.forRootAsync({
// name: 'postgres',
// inject: [ConfigService],
// useFactory: createPostgresConfig,
// }),
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
@@ -48,7 +45,6 @@ import { PartnersModule } from './partners/partners.module';
}),
}),
LogisticModule,
OrdersPaymentModule,
HttpModule,
OrdersModule,
clientes,
@@ -56,11 +52,8 @@ import { PartnersModule } from './partners/partners.module';
DataConsultModule,
AuthModule,
DebModule,
OrdersModule,
PartnersModule,
],
controllers: [LogisticController],
providers: [LogisticService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {

View File

@@ -25,7 +25,7 @@ describe('AuthService - logout', () => {
jest.clearAllMocks();
});
describe('logout - Tests that expose problems', () => {
describe('logout - Testes que expõem problemas', () => {
/**
* NOTA: Estes testes identificam problemas no método logout.
*
@@ -40,7 +40,7 @@ describe('AuthService - logout', () => {
* 8. Não sanitiza entrada
*/
it('should reject empty token', async () => {
it('deve rejeitar token vazio', async () => {
await expect(context.service.logout('')).rejects.toThrow(
'Token não pode estar vazio',
);
@@ -51,7 +51,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should reject null token', async () => {
it('deve rejeitar token null', async () => {
await expect(context.service.logout(null as any)).rejects.toThrow(
'Token não pode estar vazio',
);
@@ -62,7 +62,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should reject undefined token', async () => {
it('deve rejeitar token undefined', async () => {
await expect(context.service.logout(undefined as any)).rejects.toThrow(
'Token não pode estar vazio',
);
@@ -73,7 +73,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should reject whitespace-only token', async () => {
it('deve rejeitar token contendo apenas espaços em branco', async () => {
await expect(context.service.logout(' ')).rejects.toThrow(
'Token não pode estar vazio',
);
@@ -84,7 +84,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should reject extremely long tokens (DoS prevention)', async () => {
it('deve rejeitar tokens extremamente longos (prevenção de DoS)', async () => {
const hugeToken = 'a'.repeat(100000);
await expect(context.service.logout(hugeToken)).rejects.toThrow(
@@ -97,7 +97,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should validate decoded token is not null', async () => {
it('deve validar que token decodificado não é null', async () => {
context.mockJwtService.decode.mockReturnValue(null);
await expect(context.service.logout('invalid.token')).rejects.toThrow(
@@ -105,7 +105,7 @@ describe('AuthService - logout', () => {
);
});
it('should validate decoded token has required fields', async () => {
it('deve validar que token decodificado possui campos obrigatórios', async () => {
context.mockJwtService.decode.mockReturnValue({} as any);
await expect(context.service.logout('incomplete.token')).rejects.toThrow(
@@ -113,7 +113,7 @@ describe('AuthService - logout', () => {
);
});
it('should not add token to blacklist if already blacklisted', async () => {
it('não deve adicionar token à blacklist se já estiver na blacklist', async () => {
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(true);
await context.service.logout('already.blacklisted.token');
@@ -123,7 +123,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should validate session exists before terminating', async () => {
it('deve validar que sessão existe antes de terminar', async () => {
context.mockJwtService.decode.mockReturnValue({
id: 1,
sessionId: 'non-existent-session',
@@ -138,7 +138,7 @@ describe('AuthService - logout', () => {
).rejects.toThrow('Sessão não encontrada');
});
it('should handle decode errors gracefully', async () => {
it('deve tratar erros de decodificação de forma graciosa', async () => {
context.mockJwtService.decode.mockImplementation(() => {
throw new Error('Token inválido');
});
@@ -148,7 +148,7 @@ describe('AuthService - logout', () => {
).rejects.toThrow('Token inválido ou não pode ser decodificado');
});
it('should sanitize token input', async () => {
it('deve sanitizar entrada do token', async () => {
const maliciousToken = "'; DROP TABLE users; --";
await expect(context.service.logout(maliciousToken)).rejects.toThrow(
@@ -158,7 +158,7 @@ describe('AuthService - logout', () => {
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
});
it('should validate id is a positive number', async () => {
it('deve validar que id é um número positivo', async () => {
context.mockJwtService.decode.mockReturnValue({
id: -1,
sessionId: 'session-123',
@@ -169,7 +169,7 @@ describe('AuthService - logout', () => {
).rejects.toThrow('ID de usuário inválido no token');
});
it('should validate sessionId format if present', async () => {
it('deve validar formato do sessionId se presente', async () => {
context.mockJwtService.decode.mockReturnValue({
id: 1,
sessionId: '',
@@ -182,7 +182,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should complete logout even if session termination fails', async () => {
it('deve completar logout mesmo se terminação de sessão falhar', async () => {
context.mockJwtService.decode.mockReturnValue({
id: 1,
sessionId: 'session-123',
@@ -200,7 +200,7 @@ describe('AuthService - logout', () => {
).toHaveBeenCalledWith('valid.token');
});
it('should not throw if token is already blacklisted', async () => {
it('não deve lançar erro se token já estiver na blacklist', async () => {
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(true);
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
new Error('Token já está na blacklist'),
@@ -213,7 +213,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should validate token format before decoding', async () => {
it('deve validar formato do token antes de decodificar', async () => {
const invalidFormatToken = 'not.a.jwt.token';
await context.service.logout(invalidFormatToken);
@@ -221,7 +221,7 @@ describe('AuthService - logout', () => {
expect(context.mockJwtService.decode).toHaveBeenCalled();
});
it('should handle concurrent logout requests safely', async () => {
it('deve tratar requisições de logout concorrentes com segurança', async () => {
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
context.mockJwtService.decode.mockReturnValue({
id: 1,
@@ -241,7 +241,7 @@ describe('AuthService - logout', () => {
).toHaveBeenCalledTimes(3);
});
it('should validate decoded payload structure', async () => {
it('deve validar estrutura do payload decodificado', async () => {
context.mockJwtService.decode.mockReturnValue({
invalidField: 'value',
} as any);
@@ -258,7 +258,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should ensure token is always blacklisted on success', async () => {
it('deve garantir que token seja sempre adicionado à blacklist em caso de sucesso', async () => {
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
await context.service.logout('valid.token');
@@ -271,7 +271,7 @@ describe('AuthService - logout', () => {
).toHaveBeenCalledTimes(1);
});
it('should handle race condition when token becomes blacklisted between check and add', async () => {
it('deve tratar condição de corrida quando token é adicionado à blacklist entre verificação e adição', async () => {
/**
* Cenário: Race condition - token não estava na blacklist quando verificamos,
* mas foi adicionado por outra requisição antes de adicionarmos.
@@ -293,7 +293,7 @@ describe('AuthService - logout', () => {
).toHaveBeenCalledWith('token.with.race.condition');
});
it('should throw error if addToBlacklist fails with non-blacklist error', async () => {
it('deve lançar erro se addToBlacklist falhar com erro não relacionado à blacklist', async () => {
/**
* Cenário: Falha ao adicionar token à blacklist por outro motivo.
* Problema: Pode falhar silenciosamente.
@@ -318,7 +318,7 @@ describe('AuthService - logout', () => {
).toHaveBeenCalledWith('token.with.blacklist.error');
});
it('should verify isBlacklisted is called before addToBlacklist', async () => {
it('deve verificar que isBlacklisted é chamado antes de addToBlacklist', async () => {
/**
* Cenário: Garantir ordem correta das chamadas.
* Problema: Pode adicionar sem verificar primeiro.

View File

@@ -12,7 +12,6 @@ import {
Query,
} from '@nestjs/common';
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';

View File

@@ -63,11 +63,7 @@ export class AuthService {
throw new BadRequestException('ID de usuário inválido');
}
if (
sellerId !== null &&
sellerId !== undefined &&
sellerId < 0
) {
if (sellerId !== null && sellerId !== undefined && sellerId < 0) {
throw new BadRequestException('ID de vendedor inválido');
}
@@ -299,7 +295,7 @@ export class AuthService {
let decoded: JwtPayload | null = null;
try {
decoded = this.jwtService.decode(token) as JwtPayload;
} catch (error) {
} catch (_error) {
throw new BadRequestException(
'Token inválido ou não pode ser decodificado',
);

View File

@@ -5,7 +5,7 @@ import { RateLimitingService } from '../../services/rate-limiting.service';
describe('RateLimitingGuard - Tests that expose problems', () => {
let guard: RateLimitingGuard;
let rateLimitingService: RateLimitingService;
let _rateLimitingService: RateLimitingService;
let mockExecutionContext: ExecutionContext;
let mockGetRequest: jest.Mock;
@@ -27,7 +27,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
}).compile();
guard = module.get<RateLimitingGuard>(RateLimitingGuard);
rateLimitingService = module.get<RateLimitingService>(RateLimitingService);
_rateLimitingService = module.get<RateLimitingService>(RateLimitingService);
mockGetRequest = jest.fn().mockReturnValue({
headers: {},

View File

@@ -1,4 +1,3 @@
/* eslint-disable prettier/prettier */
export interface JwtPayload {
id: number;
sellerId: number | null;
@@ -8,4 +7,3 @@ export interface JwtPayload {
exp?: number; // Timestamp de expiração do JWT
sessionId?: string; // ID da sessão atual
}

View File

@@ -61,4 +61,3 @@ export async function createRefreshTokenServiceTestModule(
mockJwtService,
};
}

View File

@@ -389,4 +389,3 @@ describe('RefreshTokenService', () => {
});
});
});

View File

@@ -59,4 +59,3 @@ export async function createTokenBlacklistServiceTestModule(
mockJwtService,
};
}

View File

@@ -90,9 +90,9 @@ describe('TokenBlacklistService', () => {
context.mockJwtService.decode.mockReturnValue(null);
await expect(
context.service.addToBlacklist(mockToken),
).rejects.toThrow('Token inválido');
await expect(context.service.addToBlacklist(mockToken)).rejects.toThrow(
'Token inválido',
);
});
it('deve lançar erro quando decode falha', async () => {
@@ -102,9 +102,9 @@ describe('TokenBlacklistService', () => {
throw new Error('Token malformado');
});
await expect(
context.service.addToBlacklist(mockToken),
).rejects.toThrow('Erro ao adicionar token à blacklist');
await expect(context.service.addToBlacklist(mockToken)).rejects.toThrow(
'Erro ao adicionar token à blacklist',
);
});
});
@@ -169,9 +169,7 @@ describe('TokenBlacklistService', () => {
};
context.mockJwtService.decode.mockReturnValue(mockPayload);
context.mockRedisClient.get.mockRejectedValue(
new Error('Redis error'),
);
context.mockRedisClient.get.mockRejectedValue(new Error('Redis error'));
const result = await context.service.isBlacklisted(mockToken);
@@ -254,4 +252,3 @@ describe('TokenBlacklistService', () => {
});
});
});

View File

@@ -193,7 +193,6 @@ export class LoginAuditService {
const cutoffDate = new Date(
DateUtil.nowTimestamp() - 30 * 24 * 60 * 60 * 1000,
);
const cutoffDateStr = DateUtil.toBrazilString(cutoffDate, 'yyyy-MM-dd');
const oldDates = this.getDateRange(new Date('2020-01-01'), cutoffDate);
for (const date of oldDates) {

View File

@@ -62,16 +62,15 @@ export class RateLimitingService {
finalConfig.blockDurationMs,
)) as [number, number];
const [attempts, isBlockedResult] = result;
const [_attempts, isBlockedResult] = result;
return isBlockedResult === 0;
}
async recordAttempt(
ip: string,
success: boolean,
config?: Partial<RateLimitConfig>,
_config?: Partial<RateLimitConfig>,
): Promise<void> {
const finalConfig = { ...this.defaultConfig, ...config };
const key = this.buildAttemptKey(ip);
const blockKey = this.buildBlockKey(ip);

View File

@@ -80,7 +80,7 @@ export class RefreshTokenService {
sessionId: sessionId || tokenData.sessionId,
tokenId,
} as JwtPayload;
} catch (error) {
} catch (_error) {
throw new UnauthorizedException('Refresh token inválido');
}
}

View File

@@ -3,6 +3,7 @@ import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
import { JwtService } from '@nestjs/jwt';
import { JwtPayload } from '../models/jwt-payload.model';
import * as crypto from 'crypto';
@Injectable()
export class TokenBlacklistService {
@@ -32,7 +33,7 @@ export class TokenBlacklistService {
const blacklistKey = this.buildBlacklistKey(token);
const result = await this.redis.get(blacklistKey);
return result === 'blacklisted';
} catch (error) {
} catch (_error) {
return false;
}
}
@@ -64,7 +65,6 @@ export class TokenBlacklistService {
}
private hashToken(token: string): string {
const crypto = require('crypto');
return crypto
.createHash('sha256')
.update(token)

View File

@@ -61,7 +61,7 @@ describe('JwtStrategy', () => {
* - Lançar UnauthorizedException
* - Com mensagem 'Payload inválido ou incompleto'
*/
testCases.forEach(({ payload, description }) => {
testCases.forEach(({ payload, description: _description }) => {
expect(() => {
if (!payload.id || !payload.sessionId) {
throw new UnauthorizedException('Payload inválido ou incompleto');

View File

@@ -1,6 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { UserRepository } from '../users/UserRepository';
import md5 = require('md5');
import * as md5 from 'md5';
@Injectable()
export class ChangePasswordService {

View File

@@ -3,15 +3,16 @@ 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
)
`;
// SQL query would be executed here if database connection was available
// 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

@@ -31,7 +31,7 @@ export class RequestSanitizerMiddleware implements NestMiddleware {
private sanitizeString(str: string): string {
// Remover tags HTML básicas
str = str.replace(/<(|\/|[^>\/bi]|\/[^>bi]|[^\/>][^>]+|\/[^>][^>]+)>/g, '');
str = str.replace(/<[^>]*>/g, '');
// Remover scripts JavaScript
str = str.replace(/javascript:/g, '');

View File

@@ -13,7 +13,7 @@ export function IsSanitized(validationOptions?: ValidationOptions) {
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
validate(value: any, _args: ValidationArguments) {
if (typeof value !== 'string') return true; // Skip non-string values
const sqlInjectionRegex =
@@ -24,7 +24,7 @@ export function IsSanitized(validationOptions?: ValidationOptions) {
// Check for NoSQL injection patterns (MongoDB)
const noSqlInjectionRegex =
/(\$where|\$ne|\$gt|\$lt|\$gte|\$lte|\$in|\$nin|\$or|\$and|\$regex|\$options|\$elemMatch|\{.*\:.*\})/i;
/(\$where|\$ne|\$gt|\$lt|\$gte|\$lte|\$in|\$nin|\$or|\$and|\$regex|\$options|\$elemMatch|\{.*:.*\})/i;
if (noSqlInjectionRegex.test(value)) {
return false;
}
@@ -38,7 +38,7 @@ export function IsSanitized(validationOptions?: ValidationOptions) {
return true;
},
defaultMessage(args: ValidationArguments) {
defaultMessage(_args: ValidationArguments) {
return 'A entrada contém caracteres inválidos ou padrões potencialmente maliciosos';
},
},
@@ -55,7 +55,7 @@ export function IsSecureId(validationOptions?: ValidationOptions) {
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
validate(value: any, _args: ValidationArguments) {
if (typeof value !== 'string' && typeof value !== 'number')
return false;
@@ -69,7 +69,7 @@ export function IsSecureId(validationOptions?: ValidationOptions) {
// Se for número, deve ser positivo
return value > 0;
},
defaultMessage(args: ValidationArguments) {
defaultMessage(_args: ValidationArguments) {
return 'O ID fornecido não é seguro ou está em formato inválido';
},
},

View File

@@ -15,7 +15,7 @@ export class RedisClientAdapter implements IRedisClient {
try {
return JSON.parse(data);
} catch (error) {
} catch (_error) {
// If it's not valid JSON, return the raw string value
return data as T;
}

View File

@@ -6,11 +6,11 @@ export const databaseConfig = registerAs('database', () => ({
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,
},
// 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

@@ -1,6 +1,5 @@
/* eslint-disable prettier/prettier */
export class ChangePasswordModel {
id: number;
password: string;
newPassword: string;
}
export class ChangePasswordModel {
id: number;
password: string;
newPassword: string;
}

View File

@@ -1,6 +1,5 @@
/* eslint-disable prettier/prettier */
export class ExposedProduct {
ean: string;
storeId: string;
userId: number;
}
export class ExposedProduct {
ean: string;
storeId: string;
userId: number;
}

View File

@@ -1,5 +1,4 @@
/* eslint-disable prettier/prettier */
export class ResetPasswordModel {
document: string;
email: string;
}
export class ResetPasswordModel {
document: string;
email: string;
}

View File

@@ -1,9 +1,8 @@
/* eslint-disable prettier/prettier */
export class ResultModel {
constructor(
public success: boolean,
public message: string,
public data: any,
public errors: any) {};
constructor(
public success: boolean,
public message: string,
public data: any,
public errors: any,
) {}
}

View File

@@ -74,11 +74,11 @@ export async function createDataConsultServiceTestModule(
error: jest.fn(),
};
jest.spyOn(Logger.prototype, 'error').mockImplementation(
(message: any, ...optionalParams: any[]) => {
jest
.spyOn(Logger.prototype, 'error')
.mockImplementation((message: any, ...optionalParams: any[]) => {
mockLogger.error(message, ...optionalParams);
},
);
});
return {
service,

View File

@@ -623,15 +623,15 @@ describe('DataConsultService', () => {
});
it('should throw error when filter is invalid', async () => {
await expect(
context.service.products(null as any),
).rejects.toThrow(HttpException);
await expect(context.service.products(null as any)).rejects.toThrow(
HttpException,
);
await expect(
context.service.products(undefined as any),
).rejects.toThrow(HttpException);
await expect(
context.service.products('' as any),
).rejects.toThrow(HttpException);
await expect(context.service.products('' as any)).rejects.toThrow(
HttpException,
);
});
it('should log error when repository throws exception', async () => {

View File

@@ -1,6 +1,4 @@
import {
ApiTags,
} from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { Controller, Get, Param } from '@nestjs/common';
import { clientesService } from './clientes.service';

View File

@@ -1,5 +1,3 @@
/* eslint-disable prettier/prettier */
import { clientesService } from './clientes.service';
import { clientesController } from './clientes.controller';
@@ -10,10 +8,8 @@ https://docs.nestjs.com/modules
import { Module } from '@nestjs/common';
@Module({
imports: [],
controllers: [
clientesController,],
providers: [
clientesService,],
imports: [],
controllers: [clientesController],
providers: [clientesService],
})
export class clientes { }
export class clientes {}

View File

@@ -142,9 +142,7 @@ export class clientesService {
);
}
async clearCustomersCache(pattern?: string) {
const cachePattern = pattern || 'clientes:*';
async clearCustomersCache(_pattern?: string) {
// Cache clearing logic would be implemented here
}
}

View File

@@ -87,13 +87,18 @@ export class DataConsultController {
@ApiBearerAuth()
@Get('products/codauxiliar/:codauxiliar')
@ApiOperation({ summary: 'Busca produtos por código auxiliar (EAN)' })
@ApiParam({ name: 'codauxiliar', description: 'Código auxiliar (EAN) do produto' })
@ApiParam({
name: 'codauxiliar',
description: 'Código auxiliar (EAN) do produto',
})
@ApiResponse({
status: 200,
description: 'Lista de produtos encontrados por código auxiliar',
type: [ProductDto],
})
async productsByCodauxiliar(@Param('codauxiliar') codauxiliar: string): Promise<ProductDto[]> {
async productsByCodauxiliar(
@Param('codauxiliar') codauxiliar: string,
): Promise<ProductDto[]> {
return this.dataConsultService.productsByCodauxiliar(codauxiliar);
}

View File

@@ -124,7 +124,10 @@ export class DataConsultRepository {
WHERE PCPRODUT.CODPROD = :0
OR REGEXP_REPLACE(PCPRODUT.CODAUXILIAR, '[^0-9]', '') = :1
`;
const results = await this.executeQuery<ProductDto[]>(sql, [filter, cleanedFilter]);
const results = await this.executeQuery<ProductDto[]>(sql, [
filter,
cleanedFilter,
]);
return results.map((result) => new ProductDto(result));
}

View File

@@ -1,4 +1,10 @@
import { Injectable, HttpException, HttpStatus, Inject, Logger } from '@nestjs/common';
import {
Injectable,
HttpException,
HttpStatus,
Inject,
Logger,
} from '@nestjs/common';
import { DataConsultRepository } from './data-consult.repository';
import { StoreDto } from './dto/store.dto';
import { SellerDto } from './dto/seller.dto';
@@ -231,9 +237,14 @@ export class DataConsultService {
this.logger.log(`Buscando produtos por codauxiliar: ${codauxiliar}`);
try {
if (!codauxiliar || typeof codauxiliar !== 'string') {
throw new HttpException('Código auxiliar inválido', HttpStatus.BAD_REQUEST);
throw new HttpException(
'Código auxiliar inválido',
HttpStatus.BAD_REQUEST,
);
}
const products = await this.repository.findProductsByCodauxiliar(codauxiliar);
const products = await this.repository.findProductsByCodauxiliar(
codauxiliar,
);
return products.map((product) => new ProductDto(product));
} catch (error) {
this.logger.error('Erro ao buscar produtos por codauxiliar', error);

View File

@@ -1,65 +1,54 @@
/* eslint-disable prettier/prettier */
/*
https://docs.nestjs.com/controllers#controllers
*/
import { LogisticService } from './logistic.service';
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() {
return this.logisticService.getExpedicao();
}
@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);
}
}
/*
https://docs.nestjs.com/controllers#controllers
*/
import { LogisticService } from './logistic.service';
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() {
return this.logisticService.getExpedicao();
}
@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,15 +1,10 @@
/* eslint-disable prettier/prettier */
import { LogisticController } from './logistic.controller';
import { LogisticService } from './logistic.service';
import { Module } from '@nestjs/common';
@Module({
imports: [],
controllers: [
LogisticController,],
providers: [
LogisticService,],
})
export class LogisticModule { }
import { LogisticController } from './logistic.controller';
import { LogisticService } from './logistic.service';
import { Module } from '@nestjs/common';
@Module({
imports: [],
controllers: [LogisticController],
providers: [LogisticService],
})
export class LogisticModule {}

View File

@@ -1,13 +1,5 @@
import {
Get,
HttpException,
HttpStatus,
Injectable,
Query,
UseGuards,
} from '@nestjs/common';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
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 '../core/models/car-in-delivery.model';
@@ -18,21 +10,29 @@ export class LogisticService {
constructor(private readonly configService: ConfigService) {}
async getExpedicao() {
// Postgres desativado: este endpoint dependia do WMS (Postgres).
// Mantido como referencia, mas sem executar queries.
throw new HttpException(
'Integracao com WMS (Postgres) desativada. Conexao com Postgres removida.',
HttpStatus.SERVICE_UNAVAILABLE,
);
/*
const dataSource = new DataSource(createPostgresConfig(this.configService));
await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
try {
const sqlWMS = `select dados.*,
( select count(distinct v.numero_carga) quantidade_cargas_embarcadas
from volume v, carga c2
where v.numero_carga = c2.numero
and c2.data_integracao >= TO_DATE('01/02/2025', 'DD/MM/YYYY')
and TO_DATE(RIGHT(c2.observacao, 10), 'DD/MM/YYYY') = dados.dataHoje
and v.embarcado = 'S' ) quantidade_cargas_embarcadas
FROM ( select date_trunc('day', (CURRENT_DATE + INTERVAL '1 day'))::date data_saida, --TO_DATE(RIGHT(c.observacao, 10), 'DD/MM/YYYY') data_saida,
date_trunc('day', (CURRENT_DATE + INTERVAL '1 day'))::date dataHoje,
SUM(c.qt_itens_conferidos) total_itens_conferidos,
( select count(distinct v.numero_carga) quantidade_cargas_embarcadas
from volume v, carga c2
where v.numero_carga = c2.numero
and c2.data_integracao >= TO_DATE('01/02/2025', 'DD/MM/YYYY')
and TO_DATE(RIGHT(c2.observacao, 10), 'DD/MM/YYYY') = dados.dataHoje
and v.embarcado = 'S' ) quantidade_cargas_embarcadas
FROM ( select date_trunc('day', (CURRENT_DATE + INTERVAL '1 day'))::date data_saida, --TO_DATE(RIGHT(c.observacao, 10), 'DD/MM/YYYY') data_saida,
date_trunc('day', (CURRENT_DATE + INTERVAL '1 day'))::date dataHoje,
SUM(c.qt_itens_conferidos) total_itens_conferidos,
SUM(c.qt_itens_separados) total_itens_separados,
SUM(c.qt_total_itens) quantidade_total_itens,
SUM(c.qt_total_pedidos) quantidade_total,
@@ -58,7 +58,7 @@ export class LogisticService {
where dados.data_saida >= current_date
ORDER BY dados.data_saida desc `;
const sql = `SELECT COUNT(DISTINCT PCCARREG.NUMCAR) as "qtde"
const _sql = `SELECT COUNT(DISTINCT PCCARREG.NUMCAR) as "qtde"
,SUM(PCPEDI.QT * PCPRODUT.PESOBRUTO) as "totalKG"
,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_nao_iniciado"
,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NOT NULL
@@ -104,9 +104,10 @@ export class LogisticService {
await queryRunner.release();
await dataSource.destroy();
}
*/
}
async getDeliveries(placa: string) {
async getDeliveries(_placa: string) {
const dataSource = new DataSource(createOracleConfig(this.configService));
await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner();
@@ -149,7 +150,8 @@ export class LogisticService {
}
async getStatusCar(placa: string) {
const dataSource = new DataSource(createPostgresConfig(this.configService));
// Postgres desativado: este metodo deve usar Oracle.
const dataSource = new DataSource(createOracleConfig(this.configService));
await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
@@ -193,7 +195,8 @@ export class LogisticService {
}
async createCarOut(data: CarOutDelivery) {
const dataSource = new DataSource(createPostgresConfig(this.configService));
// Postgres desativado: este fluxo usa SQL Oracle (DUAL/sequence).
const dataSource = new DataSource(createOracleConfig(this.configService));
await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
@@ -205,10 +208,10 @@ export class LogisticService {
let helperId1 = 0;
let helperId2 = 0;
let helperId3 = 0;
const image1 = '';
const image2 = '';
const image3 = '';
const image4 = '';
const _image1 = '';
const _image2 = '';
const _image3 = '';
const _image4 = '';
data.helpers.forEach((helper) => {
switch (i) {
@@ -264,7 +267,8 @@ export class LogisticService {
}
async createCarIn(data: CarInDelivery) {
const dataSource = new DataSource(createPostgresConfig(this.configService));
// Postgres desativado: este fluxo usa SQL Oracle (DUAL/sequence).
const dataSource = new DataSource(createOracleConfig(this.configService));
await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
@@ -286,11 +290,11 @@ export class LogisticService {
);
}
const i = 0;
const image1 = '';
const image2 = '';
const image3 = '';
const image4 = '';
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,17 +0,0 @@
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

@@ -1,66 +0,0 @@
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.0,
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

@@ -1,91 +0,0 @@
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.0,
})
amount: number;
@ApiProperty({
description: 'Número de parcelas',
example: 3,
})
installments: number;
@ApiProperty({
description: 'Valor total pago',
example: 1000.0,
})
amountPaid: number;
constructor(partial: Partial<OrderDto>) {
Object.assign(this, partial);
}
}

View File

@@ -1,67 +0,0 @@
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.0,
})
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,82 +0,0 @@
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiResponse,
ApiBearerAuth,
} 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';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
@ApiTags('Orders Payment')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('api/v1/orders-payment')
export class OrdersPaymentController {
constructor(private readonly orderPaymentService: OrdersPaymentService) {}
@Get('orders/:id')
@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')
@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')
@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')
@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')
@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

@@ -1,23 +0,0 @@
/* eslint-disable prettier/prettier */
/*
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: [
ConfigModule,
DatabaseModule,
],
controllers: [OrdersPaymentController],
providers: [OrdersPaymentService],
exports: [OrdersPaymentService],
})
export class OrdersPaymentModule { }

View File

@@ -1,120 +0,0 @@
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): 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"
,PCPEDC.CODCLI as "customerId"
,PCCLIENT.CLIENTE as "customerName"
,PCPEDC.CODUSUR as "sellerId"
,PCUSUARI.NOME as "sellerName"
,PCPEDC.CODCOB as "billingId"
,PCCOB.COBRANCA as "billingName"
,PCPEDC.CODPLPAG as "planId"
,PCPLPAG.DESCRICAO as "planName"
,ROUND(PCPEDC.VLATEND,2) as "amount"
,NVL(PCPLPAG.NUMPARCELAS,1) as "installments"
,( SELECT SUM(ESTPAGAMENTO.VALOR) FROM ESTPAGAMENTO
WHERE ESTPAGAMENTO.NUMORCA = PCPEDC.NUMPED ) as "amountPaid"
FROM PCPEDC, PCCLIENT, PCUSUARI, PCCOB, PCPLPAG
WHERE PCPEDC.CODCLI = PCCLIENT.CODCLI
AND PCPEDC.CODUSUR = PCUSUARI.CODUSUR
AND PCPEDC.CODPLPAG = PCPLPAG.CODPLPAG
AND PCPEDC.CODCOB = PCCOB.CODCOB
AND PCPEDC.CONDVENDA = 7
AND PCPEDC.POSICAO IN ('L')
AND PCPEDC.DATA >= TRUNC(SYSDATE) - 5
AND PCPEDC.CODFILIAL = ${storeId} `;
let sqlWhere = '';
if (orderId > 0) {
sqlWhere += ` AND PCPEDC.NUMPED = ${orderId}`;
}
const orders = await queryRunner.manager.query(sql + sqlWhere);
return orders.map((order) => new OrderDto(order));
} finally {
await queryRunner.release();
}
}
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"
,ESTPAGAMENTO.CARTAO as "card"
,ESTPAGAMENTO.PARCELAS as "installments"
,ESTPAGAMENTO.NOMEBANDEIRA as "flagName"
,ESTPAGAMENTO.FORMAPAGTO as "type"
,ESTPAGAMENTO.VALOR as "amount"
,ESTPAGAMENTO.CODFUNC as "userId"
,ESTPAGAMENTO.NSU as "nsu"
,ESTPAGAMENTO.CODAUTORIZACAO as "auth"
FROM ESTPAGAMENTO
WHERE ESTPAGAMENTO.NUMORCA = ${orderId}`;
const payments = await queryRunner.manager.query(sql);
return payments.map((payment) => new PaymentDto(payment));
} finally {
await queryRunner.release();
}
}
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.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();
throw error;
} finally {
await queryRunner.release();
}
}
async createInvoice(data: CreateInvoiceDto): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const sql = `BEGIN
ESK_FATURAMENTO.FATURAMENTO_VENDA_ASSISTIDA(${data.orderId}, ${data.userId});
END;`;
await queryRunner.manager.query(sql);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
}

View File

@@ -2,9 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { DebService } from '../deb.service';
import { DebRepository } from '../../repositories/deb.repository';
export const createMockRepository = (
methods: Partial<DebRepository> = {},
) =>
export const createMockRepository = (methods: Partial<DebRepository> = {}) =>
({
findByCpfCgcent: jest.fn(),
...methods,
@@ -37,4 +35,3 @@ export async function createDebServiceTestModule(
mockRepository,
};
}

View File

@@ -81,10 +81,7 @@ describe('DebService', () => {
context.mockRepository.findByCpfCgcent.mockResolvedValue(mockDebs);
const result = await context.service.findByCpfCgcent(
'12345678900',
1498,
);
const result = await context.service.findByCpfCgcent('12345678900', 1498);
expect(result).toHaveLength(1);
expect(context.mockRepository.findByCpfCgcent).toHaveBeenCalledWith(
@@ -178,9 +175,7 @@ describe('DebService', () => {
it('deve propagar erro do repositório', async () => {
const repositoryError = new Error('Database connection failed');
context.mockRepository.findByCpfCgcent.mockRejectedValue(
repositoryError,
);
context.mockRepository.findByCpfCgcent.mockRejectedValue(repositoryError);
await expect(
context.service.findByCpfCgcent('12345678900'),
@@ -188,4 +183,3 @@ describe('DebService', () => {
});
});
});

View File

@@ -4,9 +4,7 @@ import { OrdersRepository } from '../../repositories/orders.repository';
import { IRedisClient } from '../../../core/configs/cache/IRedisClient';
import { RedisClientToken } from '../../../core/configs/cache/redis-client.adapter.provider';
export const createMockRepository = (
methods: Partial<OrdersRepository> = {},
) =>
export const createMockRepository = (methods: Partial<OrdersRepository> = {}) =>
({
findOrders: jest.fn(),
getCompletedDeliveries: jest.fn(),
@@ -57,4 +55,3 @@ export async function createOrdersServiceTestModule(
mockRedisClient,
};
}

View File

@@ -87,7 +87,9 @@ describe('OrdersService', () => {
const result = await context.service.findOrders(query);
expect(result).toEqual(mockOrders);
expect(context.mockRepository.getCompletedDeliveries).not.toHaveBeenCalled();
expect(
context.mockRepository.getCompletedDeliveries,
).not.toHaveBeenCalled();
expect(result[0].completedDeliveries).toBeUndefined();
});
@@ -139,13 +141,19 @@ describe('OrdersService', () => {
expect(result).toHaveLength(2);
expect(result[0].completedDeliveries).toEqual(mockDeliveries1);
expect(result[1].completedDeliveries).toEqual(mockDeliveries2);
expect(context.mockRepository.getCompletedDeliveries).toHaveBeenCalledTimes(2);
expect(context.mockRepository.getCompletedDeliveries).toHaveBeenCalledWith({
expect(
context.mockRepository.getCompletedDeliveries,
).toHaveBeenCalledTimes(2);
expect(
context.mockRepository.getCompletedDeliveries,
).toHaveBeenCalledWith({
orderNumber: 12345,
limit: 10,
offset: 0,
});
expect(context.mockRepository.getCompletedDeliveries).toHaveBeenCalledWith({
expect(
context.mockRepository.getCompletedDeliveries,
).toHaveBeenCalledWith({
orderNumber: 12346,
limit: 10,
offset: 0,
@@ -176,7 +184,9 @@ describe('OrdersService', () => {
expect(result).toHaveLength(1);
expect(result[0].completedDeliveries).toEqual([]);
expect(context.mockRepository.getCompletedDeliveries).toHaveBeenCalled();
expect(
context.mockRepository.getCompletedDeliveries,
).toHaveBeenCalled();
});
it('should handle empty orders array', async () => {
@@ -273,7 +283,9 @@ describe('OrdersService', () => {
const result = await context.service.findOrders(query);
expect(result).toHaveLength(1);
expect(context.mockRepository.getCompletedDeliveries).toHaveBeenCalledWith({
expect(
context.mockRepository.getCompletedDeliveries,
).toHaveBeenCalledWith({
orderNumber: null,
limit: 10,
offset: 0,
@@ -300,4 +312,3 @@ describe('OrdersService', () => {
});
});
});

View File

@@ -66,7 +66,7 @@ export class OrdersService {
deliveryQuery,
);
order.completedDeliveries = deliveries;
} catch (error) {
} catch (_error) {
order.completedDeliveries = [];
}
}
@@ -242,7 +242,7 @@ export class OrdersService {
deliveryQuery,
);
orderDelivery.completedDeliveries = deliveries;
} catch (error) {
} catch (_error) {
orderDelivery.completedDeliveries = [];
}

View File

@@ -13,7 +13,8 @@ export class FindOrdersDto {
@IsOptional()
@IsString()
@ApiPropertyOptional({
description: 'Código da filial (pode ser múltiplas filiais separadas por vírgula, ex: "1,2,3")',
description:
'Código da filial (pode ser múltiplas filiais separadas por vírgula, ex: "1,2,3")',
})
codfilial?: string;
@@ -56,7 +57,8 @@ export class FindOrdersDto {
@IsOptional()
@IsString()
@ApiPropertyOptional({
description: 'Código do usuário 2 (pode ser múltiplos valores separados por vírgula)',
description:
'Código do usuário 2 (pode ser múltiplos valores separados por vírgula)',
})
codusur2?: string;

View File

@@ -21,9 +21,10 @@ import { DeliveryCompleted } from '../dto/delivery-completed.dto';
export class OrdersRepository {
constructor(
@InjectDataSource('oracle') private readonly oracleDataSource: DataSource,
@InjectDataSource('postgres')
private readonly postgresDataSource: DataSource,
) {}
) // Postgres desativado: conexao removida e trechos de uso comentados.
// @InjectDataSource('postgres')
// private readonly postgresDataSource: DataSource,
{}
/**
* Busca log de transferência por ID do pedido
@@ -419,9 +420,10 @@ WHERE
.filter((c) => c)
.map((c) => Number(c))
.filter((c) => !isNaN(c));
const codusur2Condition = codusur2List.length === 1
? `AND PCPEDC.CODUSUR2 = :codusur2`
: `AND PCPEDC.CODUSUR2 IN (${codusur2List.join(',')})`;
const codusur2Condition =
codusur2List.length === 1
? `AND PCPEDC.CODUSUR2 = :codusur2`
: `AND PCPEDC.CODUSUR2 IN (${codusur2List.join(',')})`;
conditions.push(codusur2Condition);
}
if (query.orderId) {
@@ -1364,10 +1366,8 @@ WHERE
}
async getOrderDeliveries(orderId: string) {
const queryRunnerOracle = this.oracleDataSource.createQueryRunner();
const queryRunnerPostgres = this.postgresDataSource.createQueryRunner();
await queryRunnerOracle.connect();
await queryRunnerPostgres.connect();
try {
const sqlOracle = `SELECT PCPEDC.CODFILIAL as "storeId"
@@ -1406,75 +1406,8 @@ WHERE
const orders = await queryRunnerOracle.manager.query(sqlOracle);
// Consulta no WMS (Postgres) - Modificada para buscar pelo número do pedido
const sqlWMS = `
SELECT p.numero,
p.posicao,
CASE
WHEN p.posicao = 'F' THEN 'FATURADO'
WHEN p.posicao = 'C' THEN 'CONCLUIDO'
WHEN p.posicao = 'L' THEN 'LIBERADO'
WHEN EXISTS (
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
AND m2.data_inicio_separacao IS NULL
) AND p.posicao = 'M' THEN 'MONTADO'
WHEN EXISTS (
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
AND m2.data_inicio_separacao IS NOT NULL
AND m2.data_fim_separacao IS NULL
) AND p.posicao = 'M' THEN 'EM SEPARACAO'
WHEN EXISTS (
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
AND m2.data_fim_separacao IS NOT NULL
AND m2.data_inicio_conferencia IS NULL
) AND p.posicao = 'M' THEN 'SEPARACAO FINALIZADA'
WHEN EXISTS (
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
AND m2.data_inicio_conferencia IS NOT NULL
AND m2.data_fim_conferencia IS NULL
) AND p.posicao = 'M' THEN 'EM CONFERENCIA'
WHEN EXISTS (
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
AND m2.data_fim_conferencia IS NOT NULL
AND (SELECT COUNT(1) FROM volume v WHERE v.numero_pedido = m2.numero_pedido AND v.data_embarque IS NULL) = 0
) AND p.posicao = 'M' THEN 'CONFERENCIA FINALIZADA'
WHEN EXISTS (
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
AND m2.data_fim_conferencia IS NOT NULL
AND (SELECT COUNT(1) FROM volume v WHERE v.numero_pedido = m2.numero_pedido AND v.data_embarque IS NULL) > 0
) AND p.posicao = 'M' THEN 'EMBARCADO'
END as "situacaoPedido"
FROM pedido p
WHERE p.numero IN (
SELECT DISTINCT m.numero_pedido
FROM movimentacao m
WHERE m.numero_pedido = $1::bigint
OR m.numero_pedido IN (
SELECT CAST(o.orderId AS bigint) FROM UNNEST($2::text[]) o(orderId)
WHERE o.orderId ~ '^[0-9]+$'
)
)
`;
// Criar array com os IDs de pedidos obtidos do Oracle
const orderIds = orders.map((o) => o.orderId?.toString() || '');
// Converter orderId para número para evitar erro de tipo
const numericOrderId = parseInt(orderId, 10);
const ordersWMS = await queryRunnerPostgres.manager.query(sqlWMS, [
numericOrderId,
orderIds,
]);
// Atualizar status baseado no WMS
for (const order of orders) {
const orderWMS = ordersWMS.find(
(o) => Number(o.numero) === Number(order.orderId),
);
if (orderWMS && !order.deliveryConfirmationDate) {
order.status = orderWMS.situacaoPedido;
}
}
// Postgres desativado: consulta no WMS (Postgres) comentada.
// O status retornado aqui fica apenas com base no Oracle.
return orders;
} catch (_error) {
@@ -1484,15 +1417,12 @@ WHERE
);
} finally {
await queryRunnerOracle.release();
await queryRunnerPostgres.release();
}
}
async getLeadtimeWMS(orderId: string): Promise<LeadtimeDto[]> {
const queryRunnerPostgres = this.postgresDataSource.createQueryRunner();
const queryRunnerOracle = this.oracleDataSource.createQueryRunner();
await queryRunnerPostgres.connect();
await queryRunnerOracle.connect();
try {
@@ -1511,84 +1441,14 @@ WHERE
`;
const dataOracle = await queryRunnerOracle.manager.query(sqlOracle);
// Consulta no Postgres
const sqlPostgres = `
SELECT DADOS.ETAPA as "etapa",
DADOS.DESCRICAO_ETAPA as "descricaoEtapa",
DADOS.DATA as "data",
DADOS.CODIGO_FUNCIONARIO as "codigoFuncionario",
DADOS.NOME_FUNCIONARIO as "nomeFuncionario",
DADOS.NUMERO_PEDIDO as "numeroPedido"
FROM (
SELECT 3 AS ETAPA, 'Inicio Separação' AS DESCRICAO_ETAPA,
MIN(m.data_inicio_separacao) AS DATA,
MAX(m.codigo_separador) AS CODIGO_FUNCIONARIO,
(SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_separador)) AS NOME_FUNCIONARIO,
m.numero_pedido
FROM movimentacao m
WHERE m.data_inicio_separacao >= '2025-01-01'
AND m.numero_pedido > 0
AND m.data_inicio_separacao IS NOT NULL
GROUP BY m.numero_pedido
UNION ALL
SELECT 4, 'Separado', MIN(m.data_fim_separacao), MAX(m.codigo_separador),
(SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_separador)), m.numero_pedido
FROM movimentacao m
WHERE m.data_inicio_separacao >= '2025-01-01'
AND m.numero_pedido > 0
AND m.data_fim_separacao IS NOT NULL
GROUP BY m.numero_pedido
UNION ALL
SELECT 5, 'Inicio Conferência', MIN(m.data_inicio_conferencia), MAX(m.codigo_conferente),
(SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_conferente)), m.numero_pedido
FROM movimentacao m
WHERE m.data_inicio_conferencia IS NOT NULL
AND m.numero_pedido > 0
GROUP BY m.numero_pedido
UNION ALL
SELECT 6, 'Fim Conferência', MIN(m.data_fim_conferencia), MAX(m.codigo_conferente),
(SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_conferente)), m.numero_pedido
FROM movimentacao m
WHERE m.data_fim_conferencia IS NOT NULL
AND m.numero_pedido > 0
GROUP BY m.numero_pedido
UNION ALL
SELECT 7, 'Embarcado', MAX(v.data_embarque), v.usuario_embarque_id,
(SELECT u.nome FROM usuario u WHERE u.id = v.usuario_embarque_id), m.numero_pedido
FROM movimentacao m
JOIN volume v ON m.numero_pedido = v.numero_pedido
WHERE v.data_embarque IS NOT NULL
AND m.numero_pedido > 0
GROUP BY v.usuario_embarque_id, m.numero_pedido
) DADOS
WHERE DADOS.numero_pedido = $1
ORDER BY DADOS.numero_pedido, DADOS.ETAPA;
`;
const dataPostgres = await queryRunnerPostgres.manager.query(
sqlPostgres,
[orderId],
);
// Junta os dados Oracle + Postgres
const leadtime = [...dataOracle, ...dataPostgres];
// Ordena pela etapa (opcional, para garantir ordem)
return leadtime.sort((a, b) => a.etapa - b.etapa);
// Postgres desativado: consulta do leadtime no WMS (Postgres) comentada.
return dataOracle;
} catch (_error) {
throw new HttpException(
'Erro ao buscar dados de leadtime do WMS',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
await queryRunnerPostgres.release();
await queryRunnerOracle.release();
}
}

View File

@@ -0,0 +1,317 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProductsController } from '../products.controller';
import { ProductsService } from '../products.service';
import { Oferta8026QueryDto } from '../dto/oferta-8026-query.dto';
import { Placa8122ResponseDto } from '../dto/placa-8122-response.dto';
import { HttpException, HttpStatus } from '@nestjs/common';
describe('ProductsController', () => {
let controller: ProductsController;
let service: ProductsService;
const mockProductsService = {
getPlaca8122: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ProductsController],
providers: [
{
provide: ProductsService,
useValue: mockProductsService,
},
],
}).compile();
controller = module.get<ProductsController>(ProductsController);
service = module.get<ProductsService>(ProductsService);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getPlaca8122', () => {
it('deve retornar lista de placas com sucesso', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [62602, 62603],
numregiao: 1,
};
const mockResponse: Placa8122ResponseDto[] = [
{
codprod: 62602,
descricao: 'PRODUTO EXEMPLO 1',
marca: 'MARCA EXEMPLO',
unidade: 'UN',
pvenda1: 99.9,
precofixo: 79.9,
dec: '99',
preco: 79,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 42,
mensagem2: null,
mensagem3: null,
mensagem4: '10X DE',
mensagem6: null,
mensagem7: null,
mensagem8: 'TOTAL: R$',
parcelas: 7.99,
inicio: new Date('2024-11-19'),
},
{
codprod: 62603,
descricao: 'PRODUTO EXEMPLO 2',
marca: 'MARCA EXEMPLO 2',
unidade: 'UN',
pvenda1: 149.9,
precofixo: 119.9,
dec: '99',
preco: 119,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 46,
mensagem2: null,
mensagem3: null,
mensagem4: '10X DE',
mensagem6: null,
mensagem7: null,
mensagem8: 'TOTAL: R$',
parcelas: 11.99,
inicio: new Date('2024-11-19'),
},
];
mockProductsService.getPlaca8122.mockResolvedValue(mockResponse);
const result = await controller.getPlaca8122(query);
expect(result).toEqual(mockResponse);
expect(result).toHaveLength(2);
expect(result[0].codprod).toBe(62602);
expect(result[0].descricao).toBe('PRODUTO EXEMPLO 1');
expect(result[0].marca).toBe('MARCA EXEMPLO');
expect(result[0].pvenda1).toBe(99.9);
expect(result[0].precofixo).toBe(79.9);
expect(result[0].percdesconto).toBe(20);
expect(result[0].mensagem4).toBe('10X DE');
expect(result[0].parcelas).toBe(7.99);
expect(service.getPlaca8122).toHaveBeenCalledTimes(1);
expect(service.getPlaca8122).toHaveBeenCalledWith(query);
});
it('deve retornar array vazio quando nenhuma placa é encontrada', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [99999],
numregiao: 1,
};
mockProductsService.getPlaca8122.mockResolvedValue([]);
const result = await controller.getPlaca8122(query);
expect(result).toEqual([]);
expect(result).toHaveLength(0);
expect(service.getPlaca8122).toHaveBeenCalledTimes(1);
expect(service.getPlaca8122).toHaveBeenCalledWith(query);
});
it('deve propagar exceção quando service lança erro de validação', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [],
numregiao: 1,
};
const error = new HttpException(
'É necessário informar pelo menos um código de produto.',
HttpStatus.BAD_REQUEST,
);
mockProductsService.getPlaca8122.mockRejectedValue(error);
await expect(controller.getPlaca8122(query)).rejects.toThrow(
HttpException,
);
await expect(controller.getPlaca8122(query)).rejects.toThrow(
'É necessário informar pelo menos um código de produto.',
);
expect(service.getPlaca8122).toHaveBeenCalledTimes(2);
expect(service.getPlaca8122).toHaveBeenCalledWith(query);
});
it('deve retornar placa com mensagem para pagamento à vista', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [62602],
numregiao: 1,
};
const mockResponse: Placa8122ResponseDto[] = [
{
codprod: 62602,
descricao: 'PRODUTO EXEMPLO',
marca: 'MARCA EXEMPLO',
unidade: 'UN',
pvenda1: 99.9,
precofixo: 79.9,
dec: '99',
preco: 79,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 10,
mensagem2: null,
mensagem3: 'À VISTA | R$',
mensagem4: null,
mensagem6: 'OU R$',
mensagem7: 'NO CARTÃO',
mensagem8: null,
parcelas: 0,
inicio: new Date('2024-11-19'),
},
];
mockProductsService.getPlaca8122.mockResolvedValue(mockResponse);
const result = await controller.getPlaca8122(query);
expect(result).toHaveLength(1);
expect(result[0].codplpagmax).toBe(10);
expect(result[0].mensagem3).toBe('À VISTA | R$');
expect(result[0].mensagem6).toBe('OU R$');
expect(result[0].mensagem7).toBe('NO CARTÃO');
expect(result[0].parcelas).toBe(0);
});
it('deve retornar placa com mensagem para débito', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [62602],
numregiao: 1,
};
const mockResponse: Placa8122ResponseDto[] = [
{
codprod: 62602,
descricao: 'PRODUTO EXEMPLO',
marca: 'MARCA EXEMPLO',
unidade: 'UN',
pvenda1: 99.9,
precofixo: 79.9,
dec: '99',
preco: 79,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 2,
mensagem2: 'DEBITO',
mensagem3: null,
mensagem4: null,
mensagem6: null,
mensagem7: null,
mensagem8: null,
parcelas: 0,
inicio: new Date('2024-11-19'),
},
];
mockProductsService.getPlaca8122.mockResolvedValue(mockResponse);
const result = await controller.getPlaca8122(query);
expect(result).toHaveLength(1);
expect(result[0].codplpagmax).toBe(2);
expect(result[0].mensagem2).toBe('DEBITO');
});
it('deve processar múltiplos produtos corretamente', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [62602, 62603, 62604],
numregiao: 1,
};
const mockResponse: Placa8122ResponseDto[] = [
{
codprod: 62602,
descricao: 'PRODUTO 1',
marca: 'MARCA 1',
unidade: 'UN',
pvenda1: 99.9,
precofixo: 79.9,
dec: '99',
preco: 79,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 42,
mensagem2: null,
mensagem3: null,
mensagem4: '10X DE',
mensagem6: null,
mensagem7: null,
mensagem8: 'TOTAL: R$',
parcelas: 7.99,
inicio: new Date('2024-11-19'),
},
{
codprod: 62603,
descricao: 'PRODUTO 2',
marca: 'MARCA 2',
unidade: 'UN',
pvenda1: 149.9,
precofixo: 119.9,
dec: '99',
preco: 119,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 46,
mensagem2: null,
mensagem3: null,
mensagem4: '10X DE',
mensagem6: null,
mensagem7: null,
mensagem8: 'TOTAL: R$',
parcelas: 11.99,
inicio: new Date('2024-11-19'),
},
{
codprod: 62604,
descricao: 'PRODUTO 3',
marca: 'MARCA 3',
unidade: 'UN',
pvenda1: 199.9,
precofixo: 159.9,
dec: '99',
preco: 159,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 42,
mensagem2: null,
mensagem3: null,
mensagem4: '10X DE',
mensagem6: null,
mensagem7: null,
mensagem8: 'TOTAL: R$',
parcelas: 15.99,
inicio: new Date('2024-11-19'),
},
];
mockProductsService.getPlaca8122.mockResolvedValue(mockResponse);
const result = await controller.getPlaca8122(query);
expect(result).toHaveLength(3);
expect(result[0].codprod).toBe(62602);
expect(result[1].codprod).toBe(62603);
expect(result[2].codprod).toBe(62604);
expect(service.getPlaca8122).toHaveBeenCalledWith(query);
});
});
});

View File

@@ -38,4 +38,3 @@ export async function createProductsServiceTestModule(
mockDataSource,
};
}

View File

@@ -1,7 +1,6 @@
import { HttpException } from '@nestjs/common';
import { createProductsServiceTestModule } from './products.service.spec.helper';
import { ProductDetailQueryDto } from '../dto/product-detail-query.dto';
import { ProductDetailResponseDto } from '../dto/product-detail-response.dto';
describe('ProductsService', () => {
describe('getProductDetails', () => {
@@ -147,12 +146,12 @@ describe('ProductsService', () => {
codfilial: '1',
};
await expect(
context.service.getProductDetails(query),
).rejects.toThrow(HttpException);
await expect(
context.service.getProductDetails(query),
).rejects.toThrow('É necessário informar codprod ou codauxiliar.');
await expect(context.service.getProductDetails(query)).rejects.toThrow(
HttpException,
);
await expect(context.service.getProductDetails(query)).rejects.toThrow(
'É necessário informar codprod ou codauxiliar.',
);
});
it('deve lançar exceção quando codauxiliar é array vazio', async () => {
@@ -162,14 +161,13 @@ describe('ProductsService', () => {
codfilial: '1',
};
await expect(
context.service.getProductDetails(query),
).rejects.toThrow(HttpException);
await expect(
context.service.getProductDetails(query),
).rejects.toThrow('É necessário informar codprod ou codauxiliar.');
await expect(context.service.getProductDetails(query)).rejects.toThrow(
HttpException,
);
await expect(context.service.getProductDetails(query)).rejects.toThrow(
'É necessário informar codprod ou codauxiliar.',
);
});
});
});
});

View File

@@ -0,0 +1,36 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsNumber, IsString, Matches } from 'class-validator';
/**
* DTO para requisição de ofertas 8026
*/
export class Oferta8026QueryDto {
@ApiProperty({
description: 'Data de início da vigência da promoção (formato DD/MM/YYYY)',
example: '19/11/2025',
})
@IsString()
@Matches(/^\d{2}\/\d{2}\/\d{4}$/, {
message: 'Data deve estar no formato DD/MM/YYYY',
})
@IsNotEmpty()
data: string;
@ApiProperty({
description: 'Array de códigos de produtos',
example: [62602],
type: [Number],
})
@IsArray()
@IsNotEmpty()
codprod: number[];
@ApiProperty({
description: 'Número da região',
example: 1,
})
@IsNumber()
@IsNotEmpty()
numregiao: number;
}

View File

@@ -0,0 +1,76 @@
import { ApiProperty } from '@nestjs/swagger';
/**
* DTO para resposta de ofertas 8026
*/
export class Oferta8026ResponseDto {
@ApiProperty({
description: 'Código do produto',
example: 12345,
})
codprod: number;
@ApiProperty({
description: 'Descrição do produto',
example: 'PRODUTO EXEMPLO',
})
descricao: string;
@ApiProperty({
description: 'Marca do produto',
example: 'MARCA EXEMPLO',
})
marca: string;
@ApiProperty({
description: 'Unidade do produto',
example: 'UN',
})
unidade: string;
@ApiProperty({
description: 'Preço de venda 1',
example: 99.9,
})
pvenda1: number;
@ApiProperty({
description: 'Preço fixo promocional',
example: 79.9,
})
precofixo: number;
@ApiProperty({
description: 'Percentual de desconto',
example: 20,
})
percdesconto: number;
@ApiProperty({
description: 'Data de fim da vigência',
example: '2024-12-31',
})
dtfimvigencia: Date;
@ApiProperty({
description: 'Mensagem para débito',
example: 'DEBITO',
required: false,
})
mensagem2: string | null;
@ApiProperty({
description: 'Mensagem para à vista',
example: 'À VISTA',
required: false,
})
mensagem3: string | null;
@ApiProperty({
description: 'Mensagem para 10x',
example: '10X',
required: false,
})
mensagem4: string | null;
}

View File

@@ -0,0 +1,127 @@
import { ApiProperty } from '@nestjs/swagger';
/**
* DTO para resposta de placa 8122
*/
export class Placa8122ResponseDto {
@ApiProperty({
description: 'Código do produto',
example: 12345,
})
codprod: number;
@ApiProperty({
description: 'Descrição do produto',
example: 'PRODUTO EXEMPLO',
})
descricao: string;
@ApiProperty({
description: 'Marca do produto',
example: 'MARCA EXEMPLO',
})
marca: string;
@ApiProperty({
description: 'Unidade do produto',
example: 'UN',
})
unidade: string;
@ApiProperty({
description: 'Preço de venda 1',
example: 99.9,
})
pvenda1: number;
@ApiProperty({
description: 'Preço fixo promocional',
example: 79.9,
})
precofixo: number;
@ApiProperty({
description: 'Parte decimal do preço',
example: '99',
})
dec: string;
@ApiProperty({
description: 'Preço truncado',
example: 79,
})
preco: number;
@ApiProperty({
description: 'Percentual de desconto',
example: 20,
})
percdesconto: number;
@ApiProperty({
description: 'Data de fim da vigência',
example: '2024-12-31',
})
dtfimvigencia: Date;
@ApiProperty({
description: 'Código do plano de pagamento máximo',
example: 42,
})
codplpagmax: number;
@ApiProperty({
description: 'Mensagem para débito',
example: 'DEBITO',
required: false,
})
mensagem2: string | null;
@ApiProperty({
description: 'Mensagem para à vista',
example: 'À VISTA | R$',
required: false,
})
mensagem3: string | null;
@ApiProperty({
description: 'Mensagem para 10x',
example: '10X DE',
required: false,
})
mensagem4: string | null;
@ApiProperty({
description: 'Mensagem 6',
example: 'OU R$',
required: false,
})
mensagem6: string | null;
@ApiProperty({
description: 'Mensagem 7',
example: 'NO CARTÃO',
required: false,
})
mensagem7: string | null;
@ApiProperty({
description: 'Mensagem 8',
example: 'TOTAL: R$',
required: false,
})
mensagem8: string | null;
@ApiProperty({
description: 'Valor das parcelas',
example: 15.99,
})
parcelas: number;
@ApiProperty({
description: 'Data de início da vigência',
example: '2024-11-19',
})
inicio: Date;
}

View File

@@ -1,5 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
import {
IsArray,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
/**
* DTO para requisição de detalhes de produtos
@@ -14,7 +20,8 @@ export class ProductDetailQueryDto {
numregiao: number;
@ApiProperty({
description: 'Array de códigos de produtos (opcional se codauxiliar for informado)',
description:
'Array de códigos de produtos (opcional se codauxiliar for informado)',
example: [1, 2, 3],
type: [Number],
required: false,
@@ -24,7 +31,8 @@ export class ProductDetailQueryDto {
codprod?: number[];
@ApiProperty({
description: 'Array de códigos auxiliares (opcional se codprod for informado)',
description:
'Array de códigos auxiliares (opcional se codprod for informado)',
example: ['7891234567890', '7891234567891'],
type: [String],
required: false,

View File

@@ -22,7 +22,8 @@ export class RotinaA4QueryDto {
codprod?: number;
@ApiProperty({
description: 'Código auxiliar do produto (opcional se codprod for informado)',
description:
'Código auxiliar do produto (opcional se codprod for informado)',
example: '7891234567890',
required: false,
})
@@ -37,4 +38,3 @@ export class RotinaA4QueryDto {
@IsNotEmpty()
codfilial: string;
}

View File

@@ -23,7 +23,8 @@ export class RotinaA4ResponseDto {
CODAUXILIAR: string;
@ApiProperty({
description: 'Preço normal do produto formatado como moeda brasileira (com decimais)',
description:
'Preço normal do produto formatado como moeda brasileira (com decimais)',
example: '1.109,90',
})
PRECO_NORMAL: string;
@@ -35,7 +36,8 @@ export class RotinaA4ResponseDto {
UNIDADE: string;
@ApiProperty({
description: 'Valor de venda formatado como moeda brasileira (sem decimais)',
description:
'Valor de venda formatado como moeda brasileira (sem decimais)',
example: 'R$ 2.499',
})
VALOR_VENDA: string;
@@ -52,4 +54,3 @@ export class RotinaA4ResponseDto {
})
MARCA: string;
}

View File

@@ -29,4 +29,3 @@ export class UnifiedProductSearchDto {
@IsNotEmpty()
codfilial: string;
}

View File

@@ -1,20 +1,35 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Get,
Param,
Post,
Query,
// UseGuards, // Comentado para uso futuro
} from '@nestjs/common';
import { ProductsService } from './products.service';
import { ExposedProduct } from 'src/core/models/exposed-product.model';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
// import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; // Comentado para uso futuro
import { ExposedProductDto } from './dto/exposed-product.dto';
import { ProductValidationDto } from './dto/ProductValidationDto';
import { ProductEcommerceDto } from './dto/product-ecommerce.dto';
import { ApiTags, ApiOperation, ApiParam, ApiQuery, ApiBody, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiQuery,
ApiBody,
ApiResponse,
// ApiBearerAuth, // Comentado para uso futuro
} from '@nestjs/swagger';
import { ProductDetailQueryDto } from './dto/product-detail-query.dto';
import { ProductDetailResponseDto } from './dto/product-detail-response.dto';
import { RotinaA4QueryDto } from './dto/rotina-a4-query.dto';
import { RotinaA4ResponseDto } from './dto/rotina-a4-response.dto';
import { UnifiedProductSearchDto } from './dto/unified-product-search.dto';
import { Oferta8026QueryDto } from './dto/oferta-8026-query.dto';
import { Oferta8026ResponseDto } from './dto/oferta-8026-response.dto';
import { Placa8122ResponseDto } from './dto/placa-8122-response.dto';
//@ApiBearerAuth()
//@UseGuards(JwtAuthGuard)
@@ -23,43 +38,57 @@ import { UnifiedProductSearchDto } from './dto/unified-product-search.dto';
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
///enpoit produtos-ecommecer
///enpoit produtos-ecommecer
@Get('products-ecommerce')
@ApiOperation({ summary: 'Lista produtos para o e-commerce' })
@ApiResponse({
status: 200,
@ApiResponse({
status: 200,
description: 'Lista de produtos retornada com sucesso.',
type: ProductEcommerceDto,
isArray: true
isArray: true,
})
///ENDPOIT DE VALIDAR PRODUTO POR FILTRO
@Get('product-validation/:storeId/:filtro')
@ApiOperation({ summary: 'Valida produto pelo filtro (código, EAN ou descrição)' })
@ApiOperation({
summary: 'Valida produto pelo filtro (código, EAN ou descrição)',
})
@ApiParam({ name: 'storeId', type: String, description: 'ID da loja' })
@ApiParam({ name: 'filtro', type: String, description: 'Filtro de busca (código, EAN ou descrição)' })
@ApiQuery({ name: 'tipoBusca', required: false, enum: ['codauxiliar', 'codprod', 'descricao', 'todos'], description: 'Tipo de busca específica (opcional). Padrão: busca em todos os campos' })
@ApiResponse({
status: 200,
description: 'Produto encontrado com sucesso.',
type: ProductValidationDto
@ApiParam({
name: 'filtro',
type: String,
description: 'Filtro de busca (código, EAN ou descrição)',
})
@ApiQuery({
name: 'tipoBusca',
required: false,
enum: ['codauxiliar', 'codprod', 'descricao', 'todos'],
description:
'Tipo de busca específica (opcional). Padrão: busca em todos os campos',
})
@ApiResponse({
status: 200,
description: 'Produto encontrado com sucesso.',
type: ProductValidationDto,
})
@ApiResponse({ status: 404, description: 'Produto não localizado.' })
async productValidation(
@Param('storeId') storeId: string,
@Param('filtro') filtro: string,
@Query('tipoBusca') tipoBusca?: 'codauxiliar' | 'codprod' | 'descricao' | 'todos',
@Query('tipoBusca')
tipoBusca?: 'codauxiliar' | 'codprod' | 'descricao' | 'todos',
): Promise<ProductValidationDto> {
return this.productsService.productsValidation(storeId, filtro, tipoBusca);
}
/// ENDPOIT PRODUTOS EXPOSTOS
/// ENDPOIT PRODUTOS EXPOSTOS
@Post('exposed-product')
@ApiOperation({ summary: 'Registra produto em exposição' })
@ApiBody({ type: ExposedProductDto })
@ApiResponse({ status: 201, description: 'Produto exposto registrado com sucesso.' })
@ApiResponse({
status: 201,
description: 'Produto exposto registrado com sucesso.',
})
async exposedProduct(@Body() exposedProduct: ExposedProduct) {
return this.productsService.exposedProduct(exposedProduct);
}
@@ -70,14 +99,16 @@ export class ProductsController {
@Post('product-details')
@ApiOperation({ summary: 'Busca detalhes de produtos com preço e estoque' })
@ApiBody({ type: ProductDetailQueryDto })
@ApiResponse({
status: 200,
description: 'Lista de produtos com detalhes retornada com sucesso.',
@ApiResponse({
status: 200,
description: 'Lista de produtos com detalhes retornada com sucesso.',
type: ProductDetailResponseDto,
isArray: true
isArray: true,
})
@ApiResponse({ status: 400, description: 'Parâmetros inválidos.' })
async getProductDetails(@Body() query: ProductDetailQueryDto): Promise<ProductDetailResponseDto[]> {
async getProductDetails(
@Body() query: ProductDetailQueryDto,
): Promise<ProductDetailResponseDto[]> {
return this.productsService.getProductDetails(query);
}
@@ -87,13 +118,18 @@ export class ProductsController {
@Post('rotina-A4')
@ApiOperation({ summary: 'Busca informações do produto conforme rotina A4' })
@ApiBody({ type: RotinaA4QueryDto })
@ApiResponse({
status: 200,
description: 'Dados do produto retornados com sucesso.',
type: RotinaA4ResponseDto
@ApiResponse({
status: 200,
description: 'Dados do produto retornados com sucesso.',
type: RotinaA4ResponseDto,
})
@ApiResponse({ status: 404, description: 'Produto não encontrado para os parâmetros informados.' })
async getRotinaA4(@Body() query: RotinaA4QueryDto): Promise<RotinaA4ResponseDto> {
@ApiResponse({
status: 404,
description: 'Produto não encontrado para os parâmetros informados.',
})
async getRotinaA4(
@Body() query: RotinaA4QueryDto,
): Promise<RotinaA4ResponseDto> {
return this.productsService.getRotinaA4(query);
}
@@ -101,16 +137,59 @@ export class ProductsController {
* Endpoint para busca unificada de produtos por nome, código de barras ou codprod
*/
@Post('unified-search')
@ApiOperation({ summary: 'Busca unificada de produtos por nome, código de barras ou codprod' })
@ApiOperation({
summary:
'Busca unificada de produtos por nome, código de barras ou codprod',
})
@ApiBody({ type: UnifiedProductSearchDto })
@ApiResponse({
status: 200,
description: 'Lista de produtos encontrados retornada com sucesso.',
@ApiResponse({
status: 200,
description: 'Lista de produtos encontrados retornada com sucesso.',
type: ProductDetailResponseDto,
isArray: true
isArray: true,
})
@ApiResponse({ status: 400, description: 'Parâmetros inválidos.' })
async unifiedProductSearch(@Body() query: UnifiedProductSearchDto): Promise<ProductDetailResponseDto[]> {
async unifiedProductSearch(
@Body() query: UnifiedProductSearchDto,
): Promise<ProductDetailResponseDto[]> {
return this.productsService.unifiedProductSearch(query);
}
/**
* Endpoint para buscar ofertas 8026
*/
@Post('oferta-8026')
@ApiOperation({ summary: 'Busca ofertas 8026 conforme parâmetros específicos' })
@ApiBody({ type: Oferta8026QueryDto })
@ApiResponse({
status: 200,
description: 'Oferta 8026 retornada com sucesso.',
type: Oferta8026ResponseDto,
isArray: true,
})
@ApiResponse({ status: 400, description: 'Parâmetros inválidos.' })
async getOferta8026(
@Body() query: Oferta8026QueryDto,
): Promise<Oferta8026ResponseDto[]> {
return this.productsService.getOferta8026(query);
}
/**
* Endpoint para buscar placa 8122
*/
@Post('placa-8122')
@ApiOperation({ summary: 'Busca placa 8122 conforme parâmetros específicos' })
@ApiBody({ type: Oferta8026QueryDto })
@ApiResponse({
status: 200,
description: 'Placa 8122 retornada com sucesso.',
type: Placa8122ResponseDto,
isArray: true,
})
@ApiResponse({ status: 400, description: 'Parâmetros inválidos.' })
async getPlaca8122(
@Body() query: Oferta8026QueryDto,
): Promise<Placa8122ResponseDto[]> {
return this.productsService.getPlaca8122(query);
}
}

View File

@@ -1,19 +1,14 @@
/* eslint-disable prettier/prettier */
/*
https://docs.nestjs.com/modules
*/
import { Module } from '@nestjs/common';
import { ProductsService } from './products.service';
import { ProductsController } from './products.controller';
@Module({
imports: [],
controllers: [
ProductsController,],
providers: [
ProductsService,],
})
export class ProductsModule { }
/*
https://docs.nestjs.com/modules
*/
import { Module } from '@nestjs/common';
import { ProductsService } from './products.service';
import { ProductsController } from './products.controller';
@Module({
imports: [],
controllers: [ProductsController],
providers: [ProductsService],
})
export class ProductsModule {}

View File

@@ -9,6 +9,9 @@ import { ProductDetailResponseDto } from './dto/product-detail-response.dto';
import { RotinaA4QueryDto } from './dto/rotina-a4-query.dto';
import { RotinaA4ResponseDto } from './dto/rotina-a4-response.dto';
import { UnifiedProductSearchDto } from './dto/unified-product-search.dto';
import { Oferta8026QueryDto } from './dto/oferta-8026-query.dto';
import { Oferta8026ResponseDto } from './dto/oferta-8026-response.dto';
import { Placa8122ResponseDto } from './dto/placa-8122-response.dto';
@Injectable()
export class ProductsService {
@@ -21,21 +24,23 @@ export class ProductsService {
): { whereCondition: string; useSingleParam: boolean } {
if (tipoBusca === 'codauxiliar') {
return {
whereCondition: 'PCPRODUT.CODAUXILIAR = REGEXP_REPLACE(:filtro, \'[^0-9]\', \'\')',
whereCondition:
"PCPRODUT.CODAUXILIAR = REGEXP_REPLACE(:filtro, '[^0-9]', '')",
useSingleParam: true,
};
}
if (tipoBusca === 'codprod') {
return {
whereCondition: 'PCPRODUT.CODPROD = REGEXP_REPLACE(:filtro, \'[^0-9]\', \'\')',
whereCondition:
"PCPRODUT.CODPROD = REGEXP_REPLACE(:filtro, '[^0-9]', '')",
useSingleParam: true,
};
}
if (tipoBusca === 'descricao') {
return {
whereCondition: 'PCPRODUT.DESCRICAO LIKE \'%\' || :filtro || \'%\'',
whereCondition: "PCPRODUT.DESCRICAO LIKE '%' || :filtro || '%'",
useSingleParam: true,
};
}
@@ -170,7 +175,11 @@ export class ProductsService {
const hasCodauxiliar = codauxiliar?.length > 0;
if (hasCodprod && hasCodauxiliar) {
return this.buildWhereConditionWithBoth(codprod, codauxiliar, startParamIndex);
return this.buildWhereConditionWithBoth(
codprod,
codauxiliar,
startParamIndex,
);
}
if (hasCodprod) {
@@ -178,7 +187,10 @@ export class ProductsService {
}
if (hasCodauxiliar) {
return this.buildWhereConditionWithCodauxiliar(codauxiliar, startParamIndex);
return this.buildWhereConditionWithCodauxiliar(
codauxiliar,
startParamIndex,
);
}
return { whereCondition: '', params: [], nextParamIndex: startParamIndex };
@@ -204,7 +216,11 @@ export class ProductsService {
paramIndex++;
});
const whereCondition = `(PCPRODUT.CODPROD IN (${codprodPlaceholders.join(',')}) OR REGEXP_REPLACE(PCPRODUT.CODAUXILIAR, '[^0-9]', '') IN (${codauxiliarPlaceholders.join(',')}))`;
const whereCondition = `(PCPRODUT.CODPROD IN (${codprodPlaceholders.join(
',',
)}) OR REGEXP_REPLACE(PCPRODUT.CODAUXILIAR, '[^0-9]', '') IN (${codauxiliarPlaceholders.join(
',',
)}))`;
params.push(...codprod);
codauxiliar.forEach((aux) => {
@@ -245,7 +261,9 @@ export class ProductsService {
paramIndex++;
});
const whereCondition = `REGEXP_REPLACE(PCPRODUT.CODAUXILIAR, '[^0-9]', '') IN (${placeholders.join(',')})`;
const whereCondition = `REGEXP_REPLACE(PCPRODUT.CODAUXILIAR, '[^0-9]', '') IN (${placeholders.join(
',',
)})`;
codauxiliar.forEach((aux) => {
params.push(aux.replace(/\D/g, ''));
});
@@ -307,9 +325,6 @@ export class ProductsService {
}));
}
/**
* Busca unificada de produtos por nome, código de barras ou codprod
*/
async unifiedProductSearch(
query: UnifiedProductSearchDto,
): Promise<ProductDetailResponseDto[]> {
@@ -394,7 +409,7 @@ export class ProductsService {
if (hasCodprod && hasCodauxiliar) {
return {
whereCondition:
'and (pcprodut.codprod = :3 OR REGEXP_REPLACE(pcprodut.CODAUXILIAR, \'[^0-9]\', \'\') = REGEXP_REPLACE(:4, \'[^0-9]\', \'\'))',
"and (pcprodut.codprod = :3 OR REGEXP_REPLACE(pcprodut.CODAUXILIAR, '[^0-9]', '') = REGEXP_REPLACE(:4, '[^0-9]', ''))",
params: [codprod, codauxiliar],
};
}
@@ -409,7 +424,7 @@ export class ProductsService {
if (hasCodauxiliar) {
return {
whereCondition:
'and REGEXP_REPLACE(pcprodut.CODAUXILIAR, \'[^0-9]\', \'\') = REGEXP_REPLACE(:3, \'[^0-9]\', \'\')',
"and REGEXP_REPLACE(pcprodut.CODAUXILIAR, '[^0-9]', '') = REGEXP_REPLACE(:3, '[^0-9]', '')",
params: [codauxiliar],
};
}
@@ -533,4 +548,239 @@ export class ProductsService {
return produto;
}
async getOferta8026(
query: Oferta8026QueryDto,
): Promise<Oferta8026ResponseDto[]> {
const { data, codprod, numregiao } = query;
if (!codprod || codprod.length === 0) {
throw new HttpException(
'É necessário informar pelo menos um código de produto.',
HttpStatus.BAD_REQUEST,
);
}
let dataFormatada = data;
const dateMatch = data.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (dateMatch) {
const [, part1, part2, year] = dateMatch;
const num2 = parseInt(part2, 10);
dataFormatada = num2 > 12
? `${part2}/${part1}/${year}`
: `${part1}/${part2}/${year}`;
}
const codprodPlaceholders: string[] = [];
const params: any[] = [];
let paramIndex = 0;
params.push(dataFormatada);
paramIndex++;
codprod.forEach((cod) => {
codprodPlaceholders.push(`:${paramIndex}`);
params.push(cod);
paramIndex++;
});
params.push(numregiao);
const sql = `
SELECT
pctabpr.codprod,
pcprodut.descricao,
pcmarca.marca,
pcprodut.unidade,
pctabpr.pvenda1,
pcprodut.codauxiliar,
pcprecoprom.precofixo,
TRUNC(((pctabpr.pvenda1 - pcprecoprom.precofixo) / pctabpr.pvenda1) * 100, 0) percdesconto,
pcprecoprom.dtfimvigencia,
CASE WHEN pcprecoprom.codplpagmax = 2 THEN 'DEBITO' ELSE NULL END mensagem2,
CASE WHEN pcprecoprom.codplpagmax = 10 THEN 'À VISTA' ELSE NULL END mensagem3,
CASE WHEN pcprecoprom.codplpagmax = 42 THEN '10X' ELSE NULL END mensagem4
FROM pctabpr, pcprecoprom, pcplpag, pcprodut, pcmarca
WHERE pctabpr.codprod = pcprecoprom.codprod
AND pctabpr.numregiao = pcprecoprom.numregiao
AND pctabpr.codprod = pcprodut.codprod
AND pcprodut.codmarca = pcmarca.codmarca (+)
AND pcprecoprom.codplpagmax = pcplpag.codplpag (+)
AND TRUNC(pcprecoprom.dtiniciovigencia) = TRUNC(TO_DATE(:0, 'DD/MM/YYYY'))
AND pcprecoprom.codprod IN (${codprodPlaceholders.join(',')})
AND PCPRECOPROM.DTFIMVIGENCIA >= TRUNC(SYSDATE)
AND pcprecoprom.codplpagmax = 42
AND pctabpr.numregiao = :${paramIndex}
`;
const result = await this.dataSource.query(sql, params);
return result.map((row) => ({
codprod: row.CODPROD,
descricao: row.DESCRICAO,
marca: row.MARCA,
unidade: row.UNIDADE,
pvenda1: row.PVENDA1,
codauxiliar: row.CODAUXILIAR,
precofixo: row.PRECOFIXO,
percdesconto: row.PERCDESCONTO,
dtfimvigencia: row.DTFIMVIGENCIA,
mensagem2: row.MENSAGEM2,
mensagem3: row.MENSAGEM3,
mensagem4: row.MENSAGEM4,
}));
}
async getPlaca8122(
query: Oferta8026QueryDto,
): Promise<Placa8122ResponseDto[]> {
const { data, codprod, numregiao } = query;
if (!codprod || codprod.length === 0) {
throw new HttpException(
'É necessário informar pelo menos um código de produto.',
HttpStatus.BAD_REQUEST,
);
}
let dataFormatada = data;
const dateMatch = data.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (dateMatch) {
const [, part1, part2, year] = dateMatch;
const num2 = parseInt(part2, 10);
dataFormatada = num2 > 12
? `${part2}/${part1}/${year}`
: `${part1}/${part2}/${year}`;
}
const codprodPlaceholders: string[] = [];
const params: any[] = [];
let paramIndex = 0;
params.push(dataFormatada);
paramIndex++;
codprod.forEach((cod) => {
codprodPlaceholders.push(`:${paramIndex}`);
params.push(cod);
paramIndex++;
});
const regiaoParamIndex = paramIndex;
params.push(numregiao);
paramIndex++;
const dataParamIndex2 = paramIndex;
params.push(dataFormatada);
paramIndex++;
const codprodPlaceholders2: string[] = [];
codprod.forEach((cod) => {
codprodPlaceholders2.push(`:${paramIndex}`);
params.push(cod);
paramIndex++;
});
const regiaoParamIndex2 = paramIndex;
params.push(numregiao);
const sql = `
SELECT PCTABPR.CODPROD, PCPRODUT.DESCRICAO, PCMARCA.MARCA, PCPRODUT.UNIDADE,
PCTABPR.PVENDA1, PCPRECOPROM.PRECOFIXO,
CASE WHEN PCTABPR.PVENDA1 < 500 and nvl(PCPRECOPROM.PRECOFIXO,0) = 0 THEN REPLACE(TO_CHAR((PCTABPR.PVENDA1) - TRUNC(PCTABPR.PVENDA1), '0.00'),'0.', '')
ELSE REPLACE(TO_CHAR(TRUNC(((PCPRECOPROM.PRECOFIXO /10) - TRUNC(PCPRECOPROM.PRECOFIXO /10)),2), '0.00'),'0.', '') END DEC,
TRUNC((PCPRECOPROM.PRECOFIXO),0) PRECO,
TRUNC((((PCTABPR.PVENDA1 - PCPRECOPROM.PRECOFIXO) / PCTABPR.PVENDA1) * 100),0) PERCDESCONTO,
PCPRECOPROM.DTFIMVIGENCIA, PCPRECOPROM.CODPLPAGMAX,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 2 THEN 'DEBITO' ELSE NULL END MENSAGEM2,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'À VISTA | R$' ELSE NULL END MENSAGEM3,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 42 THEN '10X DE' ELSE NULL END MENSAGEM4,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'OU R$' ELSE NULL END MENSAGEM6,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'NO CARTÃO' ELSE NULL END MENSAGEM7,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 42 THEN 'TOTAL: R$' ELSE NULL END MENSAGEM8,
CASE
WHEN NVL(PCPRECOPROM.CODPLPAGMAX,0) = 42 THEN
TRUNC((PCPRECOPROM.PRECOFIXO / 10),2)
ELSE 0
END PARCELAS,
PCPRECOPROM.DTINICIOVIGENCIA INICIO
FROM PCTABPR, PCPRECOPROM, PCPLPAG, PCPRODUT, PCMARCA
WHERE PCTABPR.CODPROD = PCPRECOPROM.CODPROD
AND PCTABPR.NUMREGIAO = PCPRECOPROM.NUMREGIAO
AND PCTABPR.CODPROD = PCPRODUT.CODPROD
AND PCPRODUT.CODMARCA = PCMARCA.CODMARCA (+)
AND PCPRECOPROM.CODPLPAGMAX = PCPLPAG.CODPLPAG (+)
AND PCPRECOPROM.DTFIMVIGENCIA >= TRUNC(SYSDATE)
AND PCPRECOPROM.CODPLPAGMAX = 42
AND PCPRECOPROM.DTINICIOVIGENCIA = TRUNC(TO_DATE(:0, 'DD/MM/YYYY'))
AND PCPRECOPROM.CODPROD IN (${codprodPlaceholders.join(',')})
AND PCTABPR.NUMREGIAO = :${regiaoParamIndex}
AND NOT EXISTS(SELECT PCFORMPROD.CODPRODMP FROM PCFORMPROD WHERE PCFORMPROD.CODPRODACAB = PCPRODUT.CODPROD)
UNION ALL
SELECT PCTABPR.CODPROD, PCPRODUT.DESCRICAO, PCMARCA.MARCA, PCPRODUT.UNIDADE,
PCTABPR.PVENDA1, PCPRECOPROM.PRECOFIXO,
CASE WHEN PCTABPR.PVENDA1 < 500 and nvl(PCPRECOPROM.PRECOFIXO,0) = 0 THEN REPLACE(TO_CHAR((PCTABPR.PVENDA1) - TRUNC(PCTABPR.PVENDA1), '0.00'),'0.', '')
ELSE REPLACE(TO_CHAR(TRUNC(((PCPRECOPROM.PRECOFIXO / 10) - TRUNC(PCPRECOPROM.PRECOFIXO / 10)),2), '0.00'),'0.', '') END DEC,
TRUNC((PCPRECOPROM.PRECOFIXO),0) PRECO,
TRUNC((((PCTABPR.PVENDA1 - PCPRECOPROM.PRECOFIXO) / PCTABPR.PVENDA1) * 100),0) PERCDESCONTO,
PCPRECOPROM.DTFIMVIGENCIA, PCPRECOPROM.CODPLPAGMAX,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 2 THEN 'DEBITO' ELSE NULL END MENSAGEM2,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'À VISTA | R$' ELSE NULL END MENSAGEM3,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 42 THEN '10X DE' ELSE NULL END MENSAGEM4,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'OU R$' ELSE NULL END MENSAGEM6,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'NO CARTÃO' ELSE NULL END MENSAGEM7,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 42 THEN 'TOTAL: R$' ELSE NULL END MENSAGEM8,
CASE
WHEN NVL(PCPRECOPROM.CODPLPAGMAX,0) = 42 THEN
TRUNC((PCPRECOPROM.PRECOFIXO / 10),2)
ELSE 0
END PARCELAS,
PCPRECOPROM.INICIO
FROM PCTABPR,
(SELECT PCFORMPROD.CODPRODACAB, PROMOCAO.DTINICIOVIGENCIA INICIO, PROMOCAO.DTINICIOVIGENCIA, PROMOCAO.NUMREGIAO, PROMOCAO.CODPLPAGMAX, SUM(NVL(PROMOCAO.PRECOFIXO, TABELA.PVENDA1) * PCFORMPROD.QTPRODMP) PRECOFIXO,
PROMOCAO.DTFIMVIGENCIA
FROM PCFORMPROD, PCPRECOPROM PROMOCAO, PCTABPR TABELA
WHERE PROMOCAO.CODPLPAGMAX = 42
AND PROMOCAO.DTINICIOVIGENCIA = TRUNC(TO_DATE(:${dataParamIndex2}, 'DD/MM/YYYY'))
AND PCFORMPROD.CODPRODMP = PROMOCAO.CODPROD
AND PCFORMPROD.CODPRODMP = TABELA.CODPROD
AND TABELA.NUMREGIAO = PROMOCAO.NUMREGIAO
GROUP BY PCFORMPROD.CODPRODACAB, PROMOCAO.NUMREGIAO, PROMOCAO.CODPLPAGMAX, PROMOCAO.DTFIMVIGENCIA, PROMOCAO.DTINICIOVIGENCIA) PCPRECOPROM, PCPLPAG, PCPRODUT, PCMARCA
WHERE PCTABPR.CODPROD = PCPRECOPROM.CODPRODACAB (+)
AND PCTABPR.NUMREGIAO = PCPRECOPROM.NUMREGIAO (+)
AND PCTABPR.CODPROD = PCPRODUT.CODPROD
AND PCPRODUT.CODMARCA = PCMARCA.CODMARCA (+)
AND PCPRECOPROM.DTFIMVIGENCIA >= TRUNC(SYSDATE)
AND PCPLPAG.CODPLPAG = 42
AND PCPRODUT.CODPROD IN (${codprodPlaceholders2.join(',')})
AND PCTABPR.NUMREGIAO = :${regiaoParamIndex2}
AND EXISTS(SELECT PCFORMPROD.CODPRODMP FROM PCFORMPROD WHERE PCFORMPROD.CODPRODACAB = PCPRODUT.CODPROD)
`;
const result = await this.dataSource.query(sql, params);
return result.map((row) => ({
codprod: row.CODPROD,
descricao: row.DESCRICAO,
marca: row.MARCA,
unidade: row.UNIDADE,
pvenda1: row.PVENDA1,
precofixo: row.PRECOFIXO,
dec: row.DEC,
preco: row.PRECO,
percdesconto: row.PERCDESCONTO,
dtfimvigencia: row.DTFIMVIGENCIA,
codplpagmax: row.CODPLPAGMAX,
mensagem2: row.MENSAGEM2,
mensagem3: row.MENSAGEM3,
mensagem4: row.MENSAGEM4,
mensagem6: row.MENSAGEM6,
mensagem7: row.MENSAGEM7,
mensagem8: row.MENSAGEM8,
parcelas: row.PARCELAS,
inicio: row.INICIO,
}));
}
}