Compare commits

..

10 Commits

Author SHA1 Message Date
Joelson
23c91d7434 Merge pull request #12 from JurunenseDesenvolvimento/homologacao
Some checks failed
CI / Lint and Format Check (push) Successful in 11m32s
CI / Build (push) Failing after 6m1s
CI / Unit Tests (push) Failing after 6m42s
CI / E2E Tests (push) Has been skipped
CI / Docker Build (push) Has been skipped
CI / Create Release (push) Has been skipped
Homologacao
2025-11-21 17:25:55 -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
Joelson
1538657c3b Merge pull request #11 from JurunenseDesenvolvimento/homologacao
fix: adiciona variáveis globais do Jest no ESLint config
2025-11-21 16:48:49 -03:00
JuruSysadmin
32da5b1466 fix: adiciona variáveis globais do Jest no ESLint config 2025-11-21 16:46:18 -03:00
Joelson
5483040fcd Merge pull request #10 from JurunenseDesenvolvimento/homologacao
Homologacao
2025-11-14 17:39:04 -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
62 changed files with 960 additions and 1026 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

@@ -4,14 +4,11 @@ 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';
@@ -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

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

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

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

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

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

@@ -1,6 +1,5 @@
/* eslint-disable prettier/prettier */
export class ChangePasswordModel {
id: number;
password: string;
newPassword: string;
}
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;
ean: string;
storeId: string;
userId: number;
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable prettier/prettier */
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,5 +1,3 @@
/* eslint-disable prettier/prettier */
/*
https://docs.nestjs.com/controllers#controllers
*/
@@ -7,59 +5,50 @@ 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';
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) {}
@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);
}
@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,],
imports: [],
controllers: [LogisticController],
providers: [LogisticService],
})
export class LogisticModule { }
export class LogisticModule {}

View File

@@ -1,11 +1,4 @@
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';
@@ -58,7 +51,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
@@ -106,7 +99,7 @@ export class LogisticService {
}
}
async getDeliveries(placa: string) {
async getDeliveries(_placa: string) {
const dataSource = new DataSource(createOracleConfig(this.configService));
await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner();
@@ -205,10 +198,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) {
@@ -286,11 +279,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

@@ -419,9 +419,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) {

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

@@ -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,21 +1,33 @@
/* 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';
//@ApiBearerAuth()
//@UseGuards(JwtAuthGuard)
@ApiTags('Produtos')
@@ -23,43 +35,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,
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' })
@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
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
@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);
}
@@ -74,10 +100,12 @@ export class ProductsController {
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);
}
@@ -90,10 +118,15 @@ export class ProductsController {
@ApiResponse({
status: 200,
description: 'Dados do produto retornados com sucesso.',
type: RotinaA4ResponseDto
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 +134,21 @@ 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.',
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);
}
}

View File

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

View File

@@ -21,21 +21,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 +172,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 +184,10 @@ export class ProductsService {
}
if (hasCodauxiliar) {
return this.buildWhereConditionWithCodauxiliar(codauxiliar, startParamIndex);
return this.buildWhereConditionWithCodauxiliar(
codauxiliar,
startParamIndex,
);
}
return { whereCondition: '', params: [], nextParamIndex: startParamIndex };
@@ -204,7 +213,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 +258,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, ''));
});
@@ -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],
};
}