feat: Implement initial NestJS API project structure including user authentication, order management, and basic configurations.

This commit is contained in:
JuruSysadmin 2026-01-07 18:12:25 -03:00
commit 51d85aeb9a
34 changed files with 13796 additions and 0 deletions

56
.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

12
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start:dev",
"problemMatcher": [],
"label": "npm: start:dev",
"detail": "nest start --watch"
}
]
}

128
README.md Normal file
View File

@ -0,0 +1,128 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Configuração do Banco de Dados Oracle
Este projeto utiliza o banco de dados Oracle. Para configurar a conexão, crie um arquivo `.env` na raiz do projeto com as seguintes variáveis:
```env
# Configurações do Banco Oracle
ORACLE_USER=seu_usuario
ORACLE_PASSWORD=sua_senha
ORACLE_CONNECTION_STRING=localhost:1521/seu_sid
ORACLE_LIB_DIR=/Seu/Dir/OracleAqui
# Porta da aplicação
PORT=3001
#Chave secreta JWT
JWT_SECRET=suaChaveAqui
```
**Importante:**
- Certifique-se de ter o Oracle Instant Client instalado
- O caminho `ORACLE_LIB_DIR` deve apontar para o diretório do Oracle Instant Client
- Se as variáveis de ambiente não estiverem configuradas, a aplicação iniciará sem conexão com o banco
## Endpoints Disponíveis
- `GET /` - Página inicial (retorna "Hello World!")
- `GET /autenticar` - Endpoint de autenticação (requer configuração do banco Oracle)
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

34
eslint.config.mjs Normal file
View File

@ -0,0 +1,34 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);

8
nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

11410
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

89
package.json Normal file
View File

@ -0,0 +1,89 @@
{
"name": "controle-saida-loja",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^11.1.5",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0",
"@types/oracledb": "^6.6.2",
"bcryptjs": "^3.0.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dotenv": "^17.2.1",
"oracledb": "^6.9.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.5",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/bcryptjs": "^2.4.6",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -0,0 +1,37 @@
import {
Injectable,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { CustomJwtService } from '../jwt/jwt.service';
@Injectable()
export class JwtAuthGuard {
constructor(private readonly jwtService: CustomJwtService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Token não fornecido');
}
const token = authHeader.substring(7);
try {
const payload = this.jwtService.verifyToken(token);
request.user = {
id: payload.sub,
username: payload.username,
nome: payload.nome,
email: payload.email,
};
return true;
} catch (error) {
throw new UnauthorizedException('Token inválido ou expirado');
}
}
}

View File

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { CustomJwtService } from './jwt.service';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET || 'Jurunense@Athentic@User',
signOptions: {
expiresIn: '48h',
},
}),
],
providers: [CustomJwtService],
exports: [CustomJwtService, PassportModule],
})
export class JwtAuthModule {}

View File

@ -0,0 +1,236 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { CustomJwtService, JwtPayload } from './jwt.service';
describe('CustomJwtService', () => {
let service: CustomJwtService;
let jwtService: jest.Mocked<JwtService>;
const mockJwtService = {
sign: jest.fn(),
verify: jest.fn(),
decode: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CustomJwtService,
{
provide: JwtService,
useValue: mockJwtService,
},
],
}).compile();
service = module.get<CustomJwtService>(CustomJwtService);
jwtService = module.get(JwtService);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('generateToken', () => {
it('deve gerar um token JWT com sucesso', () => {
const payload: JwtPayload = {
sub: 1,
username: 'testuser',
nome: 'Test User',
email: 'test@example.com',
};
const expectedToken = 'mock.jwt.token';
jwtService.sign.mockReturnValue(expectedToken);
const result = service.generateToken(payload);
expect(result).toBe(expectedToken);
expect(jwtService.sign).toHaveBeenCalledWith(payload);
expect(jwtService.sign).toHaveBeenCalledTimes(1);
});
it('deve lançar erro quando payload for null', () => {
expect(() => service.generateToken(null as any)).toThrow(
'Payload inválido para geração de token',
);
expect(jwtService.sign).not.toHaveBeenCalled();
});
it('deve lançar erro quando payload for undefined', () => {
expect(() => service.generateToken(undefined as any)).toThrow(
'Payload inválido para geração de token',
);
expect(jwtService.sign).not.toHaveBeenCalled();
});
it('deve lançar erro quando houver erro na geração do token', () => {
const payload: JwtPayload = {
sub: 1,
username: 'testuser',
nome: 'Test User',
email: 'test@example.com',
};
jwtService.sign.mockImplementation(() => {
throw new Error('Erro interno');
});
expect(() => service.generateToken(payload)).toThrow('Erro ao gerar token');
expect(jwtService.sign).toHaveBeenCalledWith(payload);
});
});
describe('verifyToken', () => {
it('deve verificar e retornar o payload de um token válido', () => {
const token = 'valid.jwt.token';
const expectedPayload: JwtPayload = {
sub: 1,
username: 'testuser',
nome: 'Test User',
email: 'test@example.com',
iat: 1234567890,
};
jwtService.verify.mockReturnValue(expectedPayload);
const result = service.verifyToken(token);
expect(result).toEqual(expectedPayload);
expect(jwtService.verify).toHaveBeenCalledWith(token);
expect(jwtService.verify).toHaveBeenCalledTimes(1);
});
it('deve lançar UnauthorizedException quando token for vazio', () => {
expect(() => service.verifyToken('')).toThrow(UnauthorizedException);
expect(() => service.verifyToken('')).toThrow('Token não fornecido ou inválido');
expect(jwtService.verify).not.toHaveBeenCalled();
});
it('deve lançar UnauthorizedException quando token for apenas espaços', () => {
expect(() => service.verifyToken(' ')).toThrow(UnauthorizedException);
expect(() => service.verifyToken(' ')).toThrow('Token não fornecido ou inválido');
expect(jwtService.verify).not.toHaveBeenCalled();
});
it('deve lançar UnauthorizedException quando token for null', () => {
expect(() => service.verifyToken(null as any)).toThrow(UnauthorizedException);
expect(jwtService.verify).not.toHaveBeenCalled();
});
it('deve lançar UnauthorizedException quando token for inválido', () => {
const token = 'invalid.jwt.token';
jwtService.verify.mockImplementation(() => {
throw new Error('invalid token');
});
expect(() => service.verifyToken(token)).toThrow(UnauthorizedException);
expect(() => service.verifyToken(token)).toThrow('Token inválido ou malformado');
expect(jwtService.verify).toHaveBeenCalledWith(token);
});
it('deve lançar UnauthorizedException quando token estiver expirado', () => {
const token = 'expired.jwt.token';
jwtService.verify.mockImplementation(() => {
throw new Error('jwt expired');
});
expect(() => service.verifyToken(token)).toThrow(UnauthorizedException);
expect(() => service.verifyToken(token)).toThrow('Token expirado');
expect(jwtService.verify).toHaveBeenCalledWith(token);
});
it('deve lançar UnauthorizedException quando token for malformado', () => {
const token = 'malformed.jwt.token';
jwtService.verify.mockImplementation(() => {
throw new Error('jwt malformed');
});
expect(() => service.verifyToken(token)).toThrow(UnauthorizedException);
expect(() => service.verifyToken(token)).toThrow('Token inválido ou malformado');
expect(jwtService.verify).toHaveBeenCalledWith(token);
});
});
describe('decodeToken', () => {
it('deve decodificar um token JWT com sucesso', () => {
const token = 'mock.jwt.token';
const expectedPayload: JwtPayload = {
sub: 1,
username: 'testuser',
nome: 'Test User',
email: 'test@example.com',
iat: 1234567890,
};
jwtService.decode.mockReturnValue(expectedPayload);
const result = service.decodeToken(token);
expect(result).toEqual(expectedPayload);
expect(jwtService.decode).toHaveBeenCalledWith(token);
expect(jwtService.decode).toHaveBeenCalledTimes(1);
});
it('deve retornar null quando o token não puder ser decodificado', () => {
const token = 'invalid.token';
jwtService.decode.mockReturnValue(null);
const result = service.decodeToken(token);
expect(result).toBeNull();
expect(jwtService.decode).toHaveBeenCalledWith(token);
});
it('deve retornar null quando token for vazio', () => {
expect(service.decodeToken('')).toBeNull();
expect(jwtService.decode).not.toHaveBeenCalled();
});
it('deve retornar null quando token for apenas espaços', () => {
expect(service.decodeToken(' ')).toBeNull();
expect(jwtService.decode).not.toHaveBeenCalled();
});
it('deve retornar null quando token for null', () => {
expect(service.decodeToken(null as any)).toBeNull();
expect(jwtService.decode).not.toHaveBeenCalled();
});
it('deve decodificar token mesmo quando inválido (sem validação)', () => {
const token = 'invalid.but.decodable.token';
const expectedPayload: JwtPayload = {
sub: 1,
username: 'testuser',
nome: 'Test User',
email: 'test@example.com',
};
jwtService.decode.mockReturnValue(expectedPayload);
const result = service.decodeToken(token);
expect(result).toEqual(expectedPayload);
expect(jwtService.decode).toHaveBeenCalledWith(token);
});
it('deve retornar null quando houver erro na decodificação', () => {
const token = 'error.token';
jwtService.decode.mockImplementation(() => {
throw new Error('Decode error');
});
const result = service.decodeToken(token);
expect(result).toBeNull();
expect(jwtService.decode).toHaveBeenCalledWith(token);
});
});
});

View File

@ -0,0 +1,64 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService as NestJwtService } from '@nestjs/jwt';
export interface JwtPayload {
sub: number;
username: string;
nome: string;
email: string;
iat?: number;
exp?: number;
}
@Injectable()
export class CustomJwtService {
constructor(private readonly jwtService: NestJwtService) {}
generateToken(payload: JwtPayload): string {
if (!payload || typeof payload !== 'object') {
throw new Error('Payload inválido para geração de token');
}
try {
return this.jwtService.sign(payload);
} catch (error) {
throw new Error(
`Erro ao gerar token: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
verifyToken(token: string): JwtPayload {
if (!token || typeof token !== 'string' || token.trim().length === 0) {
throw new UnauthorizedException('Token não fornecido ou inválido');
}
try {
const payload = this.jwtService.verify<JwtPayload>(token);
return payload;
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('expired')) {
throw new UnauthorizedException('Token expirado');
}
if (error.message.includes('invalid') || error.message.includes('malformed')) {
throw new UnauthorizedException('Token inválido ou malformado');
}
}
throw new UnauthorizedException('Token inválido');
}
}
decodeToken(token: string): JwtPayload | null {
if (!token || typeof token !== 'string' || token.trim().length === 0) {
return null;
}
try {
const payload = this.jwtService.decode<JwtPayload>(token);
return payload;
} catch {
return null;
}
}
}

View File

@ -0,0 +1,128 @@
import {
Body,
Controller,
Post,
Get,
UseGuards,
Request,
} from '@nestjs/common';
import { UserService } from './user.service';
import UserDTO from 'src/models/UserDTO';
import AuthResponseDTO from 'src/models/AuthResponseDTO';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBody,
ApiBearerAuth,
ApiUnauthorizedResponse,
ApiInternalServerErrorResponse,
} from '@nestjs/swagger';
// Interface para tipar o request com usuário
interface RequestWithUser extends Request {
user: {
id: number;
username: string;
nome: string;
email: string;
};
}
@ApiTags('user')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('/autenticar')
@ApiOperation({
summary: 'Autenticar usuário',
description:
'Endpoint público para autenticação de usuários. Retorna um token JWT válido.',
})
@ApiBody({
schema: {
type: 'object',
properties: {
userName: { type: 'string', example: 'JOAO.SILVA' },
password: { type: 'string', example: '123456' },
},
},
description: 'Dados de login do usuário',
})
@ApiResponse({
status: 200,
description: 'Usuário autenticado com sucesso',
type: AuthResponseDTO,
})
@ApiUnauthorizedResponse({
description: 'Usuário ou senha inválidos',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 401 },
message: { type: 'string', example: 'Usuario ou senha invalidos' },
error: { type: 'string', example: 'Unauthorized' },
},
},
})
@ApiInternalServerErrorResponse({
description: 'Erro interno do servidor ou problema de conexão com banco',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 500 },
message: { type: 'string', example: 'Erro ao consultar view' },
error: { type: 'string', example: 'Internal Server Error' },
},
},
})
async autenticarUsuario(@Body() user: UserDTO): Promise<AuthResponseDTO> {
return await this.userService.autenticarUsuario({
userName: user.userName.toUpperCase(),
password: user.password.toUpperCase(),
});
}
@Get('/perfil')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({
summary: 'Obter perfil do usuário',
description:
'Endpoint protegido que retorna os dados do usuário autenticado',
})
@ApiResponse({
status: 200,
description: 'Perfil do usuário retornado com sucesso',
schema: {
type: 'object',
properties: {
id: { type: 'number', example: 1 },
username: { type: 'string', example: 'JOAO.SILVA' },
nome: { type: 'string', example: 'João Silva' },
email: { type: 'string', example: 'joao.silva@empresa.com' },
},
},
})
@ApiUnauthorizedResponse({
description: 'Token JWT inválido ou ausente',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 401 },
message: { type: 'string', example: 'Unauthorized' },
error: { type: 'string', example: 'Unauthorized' },
},
},
})
getProfile(@Request() req: RequestWithUser): {
id: number;
username: string;
nome: string;
email: string;
} {
return req.user;
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { JwtAuthModule } from '../jwt/jwt.module';
import { DatabaseModule } from '../../database/database.module';
@Module({
imports: [JwtAuthModule, DatabaseModule],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}

View File

@ -0,0 +1,225 @@
import { Test, TestingModule } from '@nestjs/testing';
import {
InternalServerErrorException,
UnauthorizedException,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CustomJwtService } from '../jwt/jwt.service';
import { DatabaseService } from '../../database/database.service';
import UserDTO from 'src/models/UserDTO';
import AuthResponseDTO from 'src/models/AuthResponseDTO';
describe('UserService', () => {
let service: UserService;
let jwtService: jest.Mocked<CustomJwtService>;
let databaseService: jest.Mocked<DatabaseService>;
const mockJwtService = {
generateToken: jest.fn(),
};
const mockDatabaseService = {
execute: jest.fn(),
testConnection: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: CustomJwtService,
useValue: mockJwtService,
},
{
provide: DatabaseService,
useValue: mockDatabaseService,
},
],
}).compile();
service = module.get<UserService>(UserService);
jwtService = module.get(CustomJwtService);
databaseService = module.get(DatabaseService);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('autenticarUsuario', () => {
const mockUser: UserDTO = {
userName: 'testuser',
password: 'password123',
};
const mockDbResult = {
rows: [
{
ID: 1,
USERNAME: 'testuser',
NOME: 'Test User',
EMAIL: 'test@example.com',
},
],
};
it('deve autenticar usuário com sucesso e retornar token e dados do usuário', async () => {
const expectedToken = 'mock.jwt.token';
const expectedPayload = {
sub: 1,
username: 'testuser',
nome: 'Test User',
email: 'test@example.com',
};
databaseService.execute.mockResolvedValue(mockDbResult);
jwtService.generateToken.mockReturnValue(expectedToken);
const result: AuthResponseDTO = await service.autenticarUsuario(mockUser);
expect(result).toEqual({
token: expectedToken,
user: {
id: 1,
userName: 'testuser',
nome: 'Test User',
email: 'test@example.com',
},
message: 'Autenticação realizada com sucesso',
});
expect(databaseService.execute).toHaveBeenCalledWith(
expect.stringContaining('SELECT'),
[mockUser.userName, mockUser.password],
);
expect(jwtService.generateToken).toHaveBeenCalledWith(expectedPayload);
expect(jwtService.generateToken).toHaveBeenCalledTimes(1);
});
it('deve lançar UnauthorizedException quando usuário não for encontrado', async () => {
databaseService.execute.mockResolvedValue({ rows: [] });
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
UnauthorizedException,
);
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
'Usuario ou senha invalidos',
);
expect(databaseService.execute).toHaveBeenCalledWith(
expect.stringContaining('SELECT'),
[mockUser.userName, mockUser.password],
);
expect(jwtService.generateToken).not.toHaveBeenCalled();
});
it('deve lançar UnauthorizedException quando senha estiver incorreta', async () => {
databaseService.execute.mockResolvedValue({ rows: [] });
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
UnauthorizedException,
);
expect(databaseService.execute).toHaveBeenCalled();
expect(jwtService.generateToken).not.toHaveBeenCalled();
});
it('deve lançar InternalServerErrorException quando houver erro no banco de dados', async () => {
const dbError = new Error('Erro de conexão com o banco');
databaseService.execute.mockRejectedValue(dbError);
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
InternalServerErrorException,
);
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
'Erro ao consultar view: Erro de conexão com o banco',
);
expect(databaseService.execute).toHaveBeenCalled();
expect(jwtService.generateToken).not.toHaveBeenCalled();
});
it('deve lançar InternalServerErrorException quando houver erro desconhecido', async () => {
databaseService.execute.mockRejectedValue('Erro desconhecido');
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
InternalServerErrorException,
);
await expect(service.autenticarUsuario(mockUser)).rejects.toThrow(
'Erro ao consultar view: Erro desconhecido',
);
expect(databaseService.execute).toHaveBeenCalled();
expect(jwtService.generateToken).not.toHaveBeenCalled();
});
it('deve propagar UnauthorizedException quando já lançada', async () => {
databaseService.execute.mockResolvedValue({ rows: [] });
try {
await service.autenticarUsuario(mockUser);
} catch (error) {
expect(error).toBeInstanceOf(UnauthorizedException);
expect(error.message).toBe('Usuario ou senha invalidos');
}
expect(databaseService.execute).toHaveBeenCalled();
expect(jwtService.generateToken).not.toHaveBeenCalled();
});
it('deve mapear corretamente os dados do banco para o payload JWT', async () => {
const customDbResult = {
rows: [
{
ID: 999,
USERNAME: 'customuser',
NOME: 'Custom Name',
EMAIL: 'custom@example.com',
},
],
};
databaseService.execute.mockResolvedValue(customDbResult);
jwtService.generateToken.mockReturnValue('custom.token');
await service.autenticarUsuario(mockUser);
expect(jwtService.generateToken).toHaveBeenCalledWith({
sub: 999,
username: 'customuser',
nome: 'Custom Name',
email: 'custom@example.com',
});
});
});
describe('testConnection', () => {
it('deve retornar mensagem de sucesso quando conexão for bem-sucedida', async () => {
const expectedMessage = 'Conexão com Oracle bem-sucedida!';
databaseService.testConnection.mockResolvedValue(expectedMessage);
const result = await service.testConnection();
expect(result).toBe(expectedMessage);
expect(databaseService.testConnection).toHaveBeenCalledTimes(1);
});
it('deve propagar erro quando houver falha na conexão', async () => {
const connectionError = new Error('Falha ao conectar no Oracle: timeout');
databaseService.testConnection.mockRejectedValue(connectionError);
await expect(service.testConnection()).rejects.toThrow(connectionError);
expect(databaseService.testConnection).toHaveBeenCalledTimes(1);
});
it('deve propagar erro genérico quando houver falha desconhecida', async () => {
const genericError = new Error('Erro genérico');
databaseService.testConnection.mockRejectedValue(genericError);
await expect(service.testConnection()).rejects.toThrow(genericError);
expect(databaseService.testConnection).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -0,0 +1,73 @@
import {
Injectable,
InternalServerErrorException,
UnauthorizedException,
} from '@nestjs/common';
import UserDTO from 'src/models/UserDTO';
import { CustomJwtService } from '../jwt/jwt.service';
import AuthResponseDTO from 'src/models/AuthResponseDTO';
import { DatabaseService } from '../../database/database.service';
@Injectable()
export class UserService {
constructor(
private readonly jwtService: CustomJwtService,
private readonly databaseService: DatabaseService,
) {}
async autenticarUsuario(user: UserDTO): Promise<AuthResponseDTO> {
try {
const result = await this.databaseService.execute(
`SELECT PCEMPR.matricula as id
,PCEMPR.usuariobd as userName
,PCEMPR.NOME AS nome
,PCEMPR.EMAIL AS email
FROM PCEMPR
WHERE
PCEMPR.USUARIOBD = :username AND PCEMPR.SENHABD = CRYPT(:password, USUARIOBD)`,
[user.userName, user.password],
);
const users = result.rows as any[];
if (users.length === 0) {
throw new UnauthorizedException('Usuario ou senha invalidos');
}
const userData = users[0];
// Gerar token JWT - usando as chaves corretas do Oracle (maiúsculas)
const payload = {
sub: userData.ID,
username: userData.USERNAME,
nome: userData.NOME,
email: userData.EMAIL,
};
const token = this.jwtService.generateToken(payload);
return {
token,
user: {
id: userData.ID,
userName: userData.USERNAME,
nome: userData.NOME,
email: userData.EMAIL,
},
message: 'Autenticação realizada com sucesso',
};
} catch (error) {
if (error instanceof UnauthorizedException) {
throw error;
}
throw new InternalServerErrorException(
'Erro ao consultar view: ' +
(error instanceof Error ? error.message : String(error)),
);
}
}
async testConnection(): Promise<string> {
return await this.databaseService.testConnection();
}
}

26
src/app.controller.ts Normal file
View File

@ -0,0 +1,26 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
@ApiTags('app')
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
@ApiOperation({
summary: 'Endpoint de teste',
description: 'Endpoint público para verificar se a API está funcionando',
})
@ApiResponse({
status: 200,
description: 'API funcionando corretamente',
schema: {
type: 'string',
example: 'Hello World!',
},
})
getHello(): string {
return this.appService.getHello();
}
}

19
src/app.module.ts Normal file
View File

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { UserModule } from './Auth/user/user.module';
import { OrdersModule } from './orders/orders.module';
import { DatabaseModule } from './database/database.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
DatabaseModule,
UserModule,
OrdersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

8
src/app.service.ts Normal file
View File

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

View File

@ -0,0 +1,98 @@
import { Controller, Get } from '@nestjs/common';
import { DatabaseService } from './database.service';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiInternalServerErrorResponse,
} from '@nestjs/swagger';
@ApiTags('database')
@Controller('database')
export class DatabaseController {
constructor(private readonly databaseService: DatabaseService) {}
@Get('test-connection')
@ApiOperation({
summary: 'Testar conexão com banco de dados',
description:
'Endpoint público para testar a conectividade com o banco Oracle',
})
@ApiResponse({
status: 200,
description: 'Conexão testada com sucesso',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: {
type: 'string',
example: 'Conexão estabelecida com sucesso',
},
},
},
})
@ApiInternalServerErrorResponse({
description: 'Erro na conexão com banco de dados',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: false },
message: { type: 'string', example: 'Erro ao conectar com banco' },
poolAvailable: { type: 'boolean', example: false },
poolStats: { type: 'object' },
},
},
})
async testConnection(): Promise<{
success: boolean;
message: string;
poolAvailable?: boolean;
poolStats?: unknown;
}> {
try {
const result = await this.databaseService.testConnection();
return { success: true, message: result };
} catch (error) {
const poolAvailable = await this.databaseService.isPoolAvailable();
const poolStats = await this.databaseService.getPoolStats();
return {
success: false,
message: error instanceof Error ? error.message : String(error),
poolAvailable,
poolStats: poolStats as unknown,
};
}
}
@Get('pool-status')
@ApiOperation({
summary: 'Status do pool de conexões',
description:
'Endpoint público para verificar o status do pool de conexões do banco',
})
@ApiResponse({
status: 200,
description: 'Status do pool retornado com sucesso',
schema: {
type: 'object',
properties: {
poolAvailable: { type: 'boolean', example: true },
poolStats: { type: 'object' },
},
},
})
async getPoolStatus(): Promise<{
poolAvailable: boolean;
poolStats: unknown;
}> {
const poolAvailable = await this.databaseService.isPoolAvailable();
const poolStats = await this.databaseService.getPoolStats();
return {
poolAvailable,
poolStats: poolStats as unknown,
};
}
}

View File

@ -0,0 +1,11 @@
import { Global, Module } from '@nestjs/common';
import { DatabaseService } from './database.service';
import { DatabaseController } from './database.controller';
@Global()
@Module({
controllers: [DatabaseController],
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}

View File

@ -0,0 +1,282 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import * as oracledb from 'oracledb';
@Injectable()
export class DatabaseService implements OnModuleInit {
private readonly logger = new Logger(DatabaseService.name);
private pool: oracledb.Pool | null = null;
private poolCreationPromise: Promise<oracledb.Pool> | null = null;
private lastActivityTime: number = 0;
private inactivityTimer: NodeJS.Timeout | null = null;
private readonly INACTIVITY_TIMEOUT = 20000; // 20 segundos
constructor() {
const libDir = process.env.ORACLE_LIB_DIR || '/Users/felipe/instantClient';
this.logger.log(`Oracle libDir: ${libDir}`);
oracledb.initOracleClient({ libDir });
}
async onModuleInit() {
this.logger.log('🚀 Inicializando pool de conexões Oracle...');
try {
await this.getOrCreatePool();
this.logger.log('✅ Pool Oracle inicializado com sucesso!');
} catch (error) {
this.logger.error(
'❌ Erro ao inicializar pool Oracle:',
error instanceof Error ? error.message : String(error),
);
}
}
private validateEnvironment(): boolean {
if (
!process.env.ORACLE_USER ||
!process.env.ORACLE_PASSWORD ||
!process.env.ORACLE_CONNECTION_STRING
) {
this.logger.warn('⚠️ Variáveis de ambiente do Oracle não configuradas.');
this.logger.warn(' Configure as seguintes variáveis no arquivo .env:');
this.logger.warn(' - ORACLE_USER');
this.logger.warn(' - ORACLE_PASSWORD');
this.logger.warn(' - ORACLE_CONNECTION_STRING');
return false;
}
return true;
}
private async createPool(): Promise<oracledb.Pool> {
if (!this.validateEnvironment()) {
throw new Error('Configurações do Oracle não encontradas');
}
try {
const pool = await oracledb.createPool({
user: process.env.ORACLE_USER,
password: process.env.ORACLE_PASSWORD,
connectString: process.env.ORACLE_CONNECTION_STRING,
poolMin: 1,
poolMax: 30,
poolIncrement: 1,
queueTimeout: 60000,
});
this.logger.log('✅ Pool Oracle criada com sucesso!');
return pool;
} catch (error) {
this.logger.error(
'❌ Erro ao criar pool Oracle:',
error instanceof Error ? error.message : String(error),
);
throw new Error('Falha ao criar pool Oracle');
}
}
private async getOrCreatePool(): Promise<oracledb.Pool> {
if (this.pool) {
return this.pool;
}
if (this.poolCreationPromise) {
return await this.poolCreationPromise;
}
this.poolCreationPromise = this.createPool();
try {
this.pool = await this.poolCreationPromise;
this.logger.log('✅ Pool Oracle recriada com sucesso!');
return this.pool;
} catch (error) {
this.logger.error('❌ Erro ao recriar pool Oracle:', error);
this.poolCreationPromise = null;
throw error;
} finally {
this.poolCreationPromise = null;
}
}
private updateInactivityTimer(): void {
this.lastActivityTime = Date.now();
if (this.inactivityTimer) {
clearTimeout(this.inactivityTimer);
}
this.inactivityTimer = setTimeout(() => {
this.closePoolIfInactive().catch((error) => {
this.logger.error(
'❌ Erro no timer de inatividade:',
error instanceof Error ? error.message : String(error),
);
});
}, this.INACTIVITY_TIMEOUT);
}
private async closePoolIfInactive(): Promise<void> {
const timeSinceLastActivity = Date.now() - this.lastActivityTime;
if (timeSinceLastActivity >= this.INACTIVITY_TIMEOUT && this.pool) {
this.logger.log('🔄 Fechando pool por inatividade (20s)...');
try {
await this.pool.close(20);
this.pool = null;
this.logger.log('✅ Pool fechada por inatividade');
} catch (error) {
this.logger.error(
'❌ Erro ao fechar pool:',
error instanceof Error ? error.message : String(error),
);
}
}
}
async getConnection(): Promise<oracledb.Connection> {
try {
const pool = await this.getOrCreatePool();
// Verifica se a pool está saudável
const isHealthy = await this.isPoolHealthy();
if (!isHealthy) {
this.logger.log('🔄 Pool não está saudável, recriando...');
this.pool = null;
const newPool = await this.getOrCreatePool();
this.updateInactivityTimer();
return await newPool.getConnection();
}
this.updateInactivityTimer();
return await pool.getConnection();
} catch (error) {
this.logger.error('❌ Erro ao obter conexão da pool:', error);
// Se a pool estiver fechada, tenta recriar
if (this.pool === null) {
this.logger.log('🔄 Tentando recriar pool...');
this.pool = null;
const newPool = await this.getOrCreatePool();
this.updateInactivityTimer();
return await newPool.getConnection();
}
throw error;
}
}
async execute(
sql: string,
binds: any[] = [],
options: oracledb.ExecuteOptions = {},
): Promise<any> {
let conn: oracledb.Connection | null = null;
try {
conn = await this.getConnection();
return await conn.execute(sql, binds, {
outFormat: oracledb.OUT_FORMAT_OBJECT,
autoCommit: true,
...options,
});
} catch (error) {
this.logger.error('❌ Erro ao executar query:', error);
throw error;
} finally {
if (conn) {
try {
await conn.close();
} catch (closeError) {
this.logger.error('❌ Erro ao fechar conexão:', closeError);
}
}
}
}
async testConnection(): Promise<string> {
try {
const pool = await this.getOrCreatePool();
this.updateInactivityTimer();
const conn = await pool.getConnection();
await conn.execute('SELECT 1 FROM DUAL');
await conn.close();
return 'Conexão com Oracle bem-sucedida!';
} catch (err) {
this.logger.error('❌ Erro no teste de conexão:', err);
throw new Error(
'Falha ao conectar no Oracle: ' +
(err instanceof Error ? err.message : String(err)),
);
}
}
async isPoolAvailable(): Promise<boolean> {
try {
if (!this.pool) {
await this.getOrCreatePool();
}
return !!this.pool;
} catch (error) {
this.logger.error('❌ Erro ao verificar disponibilidade da pool:', error);
return false;
}
}
async isPoolHealthy(): Promise<boolean> {
if (!this.pool) {
return false;
}
try {
const conn = await this.pool.getConnection();
await conn.execute('SELECT 1 FROM DUAL');
await conn.close();
return true;
} catch (error) {
this.logger.error('❌ Pool não está saudável:', error);
return false;
}
}
async forceClosePool(): Promise<void> {
if (this.inactivityTimer) {
clearTimeout(this.inactivityTimer);
this.inactivityTimer = null;
}
if (this.pool) {
this.logger.log('🔄 Fechando pool forçadamente...');
try {
await this.pool.close(20);
this.pool = null;
this.logger.log('✅ Pool fechada forçadamente');
} catch (error) {
this.logger.error(
'❌ Erro ao fechar pool:',
error instanceof Error ? error.message : String(error),
);
}
}
}
async getPoolStats(): Promise<any> {
try {
// Tenta obter ou criar a pool se não existir
if (!this.pool) {
await this.getOrCreatePool();
}
if (!this.pool) {
return { status: 'closed' };
}
return {
status: 'open',
lastActivity: new Date(this.lastActivityTime).toISOString(),
timeSinceLastActivity: Date.now() - this.lastActivityTime,
inactivityTimeout: this.INACTIVITY_TIMEOUT,
};
} catch (error) {
this.logger.error('❌ Erro ao obter estatísticas da pool:', error);
return {
status: 'error',
error: error instanceof Error ? error.message : String(error),
};
}
}
}

69
src/main.ts Normal file
View File

@ -0,0 +1,69 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Habilitar CORS
app.enableCors({
origin: true,
credentials: true,
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
}),
);
// Configuração do Swagger
const config = new DocumentBuilder()
.setTitle('Controle Saída Loja API')
.setDescription('API para controle de saída de loja com autenticação JWT')
.setVersion('1.0')
.addTag('auth', 'Endpoints de autenticação')
.addTag('user', 'Endpoints de usuário')
.addTag('database', 'Endpoints de banco de dados')
.addTag('orders', 'Endpoints de pedidos/notas fiscais')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'JWT',
description: 'Enter JWT token',
in: 'header',
},
'JWT-auth', // This name here is important for references
)
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document, {
swaggerOptions: {
persistAuthorization: true,
},
});
app.enableShutdownHooks();
await app.listen(process.env.PORT ?? 3001, '0.0.0.0');
console.log(`Server is running on port ${process.env.PORT ?? 3001}`);
console.log(
`Swagger documentation available at http://localhost:${process.env.PORT ?? 3001}/api`,
);
}
process.on('SIGINT', () => {
console.log('Recebido SIGINT. Encerrando aplicação...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('Recebido SIGTERM. Encerrando aplicação...');
process.exit(0);
});
bootstrap();

View File

@ -0,0 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
export default class AuthResponseDTO {
@ApiProperty({
description: 'Token JWT para autenticação',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
token: string;
@ApiProperty({
description: 'Dados do usuário autenticado',
example: {
id: 1,
userName: 'JOAO.SILVA',
nome: 'João Silva',
email: 'joao.silva@empresa.com',
},
})
user: {
id: number;
userName: string;
nome: string;
email: string;
};
@ApiProperty({
description: 'Mensagem de resposta da autenticação',
example: 'Autenticação realizada com sucesso',
})
message: string;
}

43
src/models/UserDTO.ts Normal file
View File

@ -0,0 +1,43 @@
import { IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export default class UserDTO {
@ApiProperty({
description: 'ID do usuário (opcional, gerado automaticamente)',
example: 1,
required: false,
})
id?: number;
@ApiProperty({
description: 'Nome de usuário para login',
example: 'JOAO.SILVA',
required: true,
})
@IsNotEmpty({ message: 'userName é obrigatório' })
@IsString({ message: 'userName deve ser uma string' })
userName: string;
@ApiProperty({
description: 'Nome completo do usuário',
example: 'João Silva',
required: false,
})
nome?: string;
@ApiProperty({
description: 'Email do usuário',
example: 'joao.silva@empresa.com',
required: false,
})
email?: string;
@ApiProperty({
description: 'Senha do usuário',
example: 'SENHA123',
required: true,
})
@IsNotEmpty({ message: 'password é obrigatório' })
@IsString({ message: 'password deve ser uma string' })
password: string;
}

View File

@ -0,0 +1,42 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
IsEnum,
IsNumber,
IsString,
ValidateNested,
} from 'class-validator';
export class EntregaDto {
@ApiProperty({ description: 'Número da transação de venda' })
@IsNumber()
NUMTRANSVENDA: number;
@ApiProperty({ description: 'Documento do recebedor' })
@IsString()
DOCUMENTO: string;
@ApiProperty({ description: 'Nome do recebedor' })
@IsString()
RECEBEDOR: string;
@ApiProperty({
description: 'Imagens da entrega',
type: () => EntregaImagemDto,
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => EntregaImagemDto)
IMAGENS: EntregaImagemDto[];
}
export class EntregaImagemDto {
@ApiProperty({ description: 'Tipo de imagem', enum: ['RP', 'SIG'] })
@IsEnum(['RP', 'SIG'])
TIPO: 'RP' | 'SIG';
@ApiProperty({ description: 'URL da imagem' })
@IsString()
URLIMAGEM: string;
}

View File

@ -0,0 +1 @@
export class Order {}

View File

@ -0,0 +1,268 @@
import {
Body,
Controller,
ForbiddenException,
Get,
Param,
Patch,
Request,
UseGuards,
} from '@nestjs/common';
import {
ApiBadGatewayResponse,
ApiBearerAuth,
ApiBody,
ApiForbiddenResponse,
ApiInternalServerErrorResponse,
ApiNotAcceptableResponse,
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
ApiUnauthorizedResponse,
ApiUnprocessableEntityResponse,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../Auth/guards/jwt-auth.guard';
import { EntregaDto } from './dto/orders.dto';
import { OrdersService } from './orders.service';
// Interface para tipar o request com usuário
interface RequestWithUser extends Request {
user: {
id: number;
username: string;
nome: string;
email: string;
};
}
@ApiTags('orders')
@Controller('orders')
export class OrdersController {
constructor(private readonly ordersService: OrdersService) {}
@Get('invoice/:chaveNota')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({
summary: 'Obter nota fiscal',
description:
'Endpoint protegido para obter dados de uma nota fiscal específica. Requer autenticação JWT e permissões adequadas.',
})
@ApiParam({
name: 'chaveNota',
description: 'Chave da nota fiscal',
example: '35240112345678901234567890123456789012345678',
})
@ApiResponse({
status: 200,
description: 'Nota fiscal retornada com sucesso',
schema: {
type: 'object',
properties: {
chaveNota: {
type: 'string',
example: '35240112345678901234567890123456789012345678',
},
numeroNota: { type: 'string', example: '123456' },
// Outros campos da nota fiscal...
},
},
})
@ApiNotAcceptableResponse({
description: 'Nota fiscal ja feito devolução',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 406 },
message: { type: 'string', example: 'Nota fiscal ja feito devolução' },
error: { type: 'string', example: 'Not Acceptable' },
},
},
})
@ApiUnprocessableEntityResponse({
description: 'Nota fiscal não é do tipo de Entrega',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 422 },
message: {
type: 'string',
example: 'Nota fiscal não é do tipo de Entrega',
},
error: { type: 'string', example: 'Unprocessable Entity' },
},
},
})
@ApiBadGatewayResponse({
description: 'Nota Fiscal entregue ao cliente!',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 502 },
message: {
type: 'string',
example: 'Nota Fiscal entregue ao cliente!',
},
error: { type: 'string', example: 'Bad Gateway' },
},
},
})
@ApiUnauthorizedResponse({
description: 'Token JWT inválido ou ausente',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 401 },
message: { type: 'string', example: 'Unauthorized' },
error: { type: 'string', example: 'Unauthorized' },
},
},
})
@ApiForbiddenResponse({
description: 'Usuário sem permissão para acessar a nota fiscal',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 403 },
message: {
type: 'string',
example: 'Você não tem permissão para acessar as nota fiscal',
},
error: { type: 'string', example: 'Forbidden' },
},
},
})
@ApiInternalServerErrorResponse({
description: 'Erro interno do servidor',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 500 },
message: { type: 'string', example: 'Erro interno do servidor' },
error: { type: 'string', example: 'Internal Server Error' },
},
},
})
async getNotaFiscal(
@Param('chaveNota') chaveNota: string,
@Request() req: RequestWithUser,
): Promise<any> {
const userFromToken = req.user;
const permissao = await this.ordersService.getPermissao(
userFromToken.id,
991234,
);
if (userFromToken.id !== permissao.idUsuario) {
throw new ForbiddenException(
'Você não tem permissão para acessar as nota fiscal',
);
}
const notaFiscal = await this.ordersService.getNotaFiscal(chaveNota);
return notaFiscal;
}
@Patch('updateinvoice/:chaveNota')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth('JWT-auth')
@ApiBody({
type: EntregaDto,
examples: {
'Exemplo endopint': {
value: {
NUMTRANSVENDA: 123456,
DOCUMENTO: '123123',
RECEBEDOR: 'FELIPE TESTE',
IMAGENS: [
{
TIPO: 'RP',
URLIMAGEM: 'https://testedeinsecaodeimagem.com/1234.jpeg',
},
{
TIPO: 'SIG',
URLIMAGEM: 'https://testedeinsecaodeimagem.com/1234.jpeg',
},
],
},
},
},
})
@ApiOperation({
summary: 'Atualizar registro de nota fiscal',
description:
'Endpoint protegido para atualizar o status de uma nota fiscal. Requer autenticação JWT e permissões adequadas.',
})
@ApiParam({
name: 'chaveNota',
description: 'Chave da nota fiscal',
example: '35240112345678901234567890123456789012345678',
})
@ApiResponse({
status: 200,
description: 'Registro atualizado com sucesso',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: 'Registro atualizado com sucesso' },
},
},
})
@ApiUnauthorizedResponse({
description: 'Token JWT inválido ou ausente',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 401 },
message: { type: 'string', example: 'Unauthorized' },
error: { type: 'string', example: 'Unauthorized' },
},
},
})
@ApiForbiddenResponse({
description: 'Usuário sem permissão para atualizar a nota fiscal',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 403 },
message: {
type: 'string',
example: 'Você não tem permissão para atualizar as nota fiscal',
},
error: { type: 'string', example: 'Forbidden' },
},
},
})
@ApiInternalServerErrorResponse({
description: 'Erro interno do servidor',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 500 },
message: { type: 'string', example: 'Erro interno do servidor' },
error: { type: 'string', example: 'Internal Server Error' },
},
},
})
async updateRegistro(
@Param('chaveNota') chaveNota: string,
@Request() req: RequestWithUser,
@Body() entrega: EntregaDto,
): Promise<any> {
const permissao = await this.ordersService.getPermissao(
req.user.id,
991234,
);
if (req.user.id !== permissao.idUsuario) {
throw new ForbiddenException(
'Você não tem permissão para atualizar as nota fiscal',
);
}
return this.ordersService.updateRegistro(chaveNota, req.user.id, entrega);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';
import { JwtAuthModule } from '../Auth/jwt/jwt.module';
@Module({
imports: [JwtAuthModule],
controllers: [OrdersController],
providers: [OrdersService],
})
export class OrdersModule {}

View File

@ -0,0 +1,293 @@
import {
BadGatewayException,
Injectable,
InternalServerErrorException,
NotAcceptableException,
NotFoundException,
UnprocessableEntityException,
} from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
import { EntregaDto } from './dto/orders.dto';
// Interfaces para tipagem dos dados
interface NotaFiscal {
CODFILIAL: string;
DTSAIDA: Date | string;
NUMPED: number;
CHAVENFE: string;
NUMNOTA: number;
NUMTRANSVENDA: number;
CODCLI: number;
CLIENTE: string;
CODUSUR: number;
NOME: string;
NUMITENS: number;
DTCANHOTO: Date | string | null;
DEVOL: string;
CONDVENDA: number;
}
interface Produto {
CODPROD: number;
DESCRICAO: string;
UNIDADE: string;
CODAUXILIAR: string;
MULTIPLO: number;
URLIMAGEM: string;
QT: number;
}
interface NotaFiscalComItens {
CODFILIAL: string;
DTSAIDA: Date | string;
NUMPED: number;
CHAVENFE: string;
NUMNOTA: number;
NUMTRANSVENDA: number;
CODCLI: number;
CLIENTE: string;
CODUSUR: number;
NOME: string;
NUMITENS: number;
DTCANHOTO: Date | string | null;
itens: Produto[];
}
// Interface para o resultado do banco de dados
interface DatabaseResult {
rows: any[];
rowsAffected?: number;
}
// Interface para o resultado da permissão
interface PermissaoResult {
CODUSUARIO: number;
CODROTINA: number;
ACESSO: string;
}
@Injectable()
export class OrdersService {
constructor(private readonly databaseService: DatabaseService) {}
async getNotaFiscal(chaveNota: string): Promise<NotaFiscalComItens> {
const sqlNotaFiscal = `
SELECT PCNFSAID.CODFILIAL,
TO_CHAR(PCNFSAID.DTSAIDA, 'DD/MM/YYYY') AS DTSAIDA,
PCNFSAID.NUMPED,
PCNFSAID.CHAVENFE,
PCNFSAID.NUMNOTA,
PCNFSAID.NUMTRANSVENDA,
PCNFSAID.CODCLI,
PCCLIENT.CLIENTE,
PCNFSAID.CODUSUR,
PCUSUARI.NOME,
PCNFSAID.NUMITENS,
TO_CHAR(PCNFSAID.DTCANHOTO, 'DD/MM/YYYY') AS DTCANHOTO,
NVL((SELECT 'S' FROM ESTPREDEVCLI WHERE ESTPREDEVCLI.NUMTRANSVENDA = PCNFSAID.NUMTRANSVENDA and rownum=1),'N') DEVOL,
PCNFSAID.CONDVENDA
FROM PCNFSAID, PCCLIENT, PCUSUARI
WHERE PCNFSAID.CODCLI = PCCLIENT.CODCLI
AND PCNFSAID.CODUSUR = PCUSUARI.CODUSUR
AND PCNFSAID.DTCANCEL IS NULL
AND PCNFSAID.CHAVENFE=:chaveNota`;
const sqlProduto = `
SELECT PCMOV.CODPROD,
PCPRODUT.DESCRICAO,
PCPRODUT.UNIDADE,
PCPRODUT.CODAUXILIAR,
PCPRODUT.MULTIPLO,
PCPRODUT.URLIMAGEM,
SUM(PCMOV.QT) QT
FROM PCMOV, PCPRODUT
WHERE PCMOV.CODPROD = PCPRODUT.CODPROD
AND PCMOV.NUMTRANSVENDA = :transacao
GROUP BY
PCMOV.CODPROD,
PCPRODUT.DESCRICAO,
PCPRODUT.UNIDADE,
PCPRODUT.CODAUXILIAR,
PCPRODUT.MULTIPLO,
PCPRODUT.URLIMAGEM`;
try {
const result = (await this.databaseService.execute(sqlNotaFiscal, [
chaveNota,
])) as DatabaseResult;
if (!result || !result.rows || result.rows.length === 0) {
throw new NotFoundException('Nota Fiscal não encontrada');
}
const notaFiscal = result.rows[0] as NotaFiscal;
if (notaFiscal.CONDVENDA !== 8) {
throw new UnprocessableEntityException(
'Nota fiscal não é do tipo de Entrega',
);
}
if (notaFiscal.DEVOL === 'S') {
throw new NotAcceptableException('Nota fiscal ja feito devolução');
}
if (notaFiscal.DTCANHOTO !== null) {
throw new BadGatewayException('Nota Fiscal entregue ao cliente!');
}
const itensResult = (await this.databaseService.execute(sqlProduto, [
notaFiscal.NUMTRANSVENDA,
])) as DatabaseResult;
const itens = itensResult.rows as Produto[];
const notaFiscalComItens: NotaFiscalComItens = {
CODFILIAL: notaFiscal.CODFILIAL,
DTSAIDA: notaFiscal.DTSAIDA,
NUMPED: notaFiscal.NUMPED,
CHAVENFE: notaFiscal.CHAVENFE,
NUMNOTA: notaFiscal.NUMNOTA,
NUMTRANSVENDA: notaFiscal.NUMTRANSVENDA,
CODCLI: notaFiscal.CODCLI,
CLIENTE: notaFiscal.CLIENTE,
CODUSUR: notaFiscal.CODUSUR,
NOME: notaFiscal.NOME,
NUMITENS: notaFiscal.NUMITENS,
DTCANHOTO: notaFiscal.DTCANHOTO,
itens: itens.map((item) => {
return {
CODPROD: item.CODPROD,
DESCRICAO: item.DESCRICAO,
UNIDADE: item.UNIDADE,
CODAUXILIAR: item.CODAUXILIAR,
MULTIPLO: item.MULTIPLO,
QT: item.QT,
URLIMAGEM: item.URLIMAGEM
? item.URLIMAGEM.split(';')[0].replace(
'http://167.249.211.178:8001',
'http://10.1.1.191',
)
: '',
};
}),
};
return notaFiscalComItens;
} catch (error) {
throw new InternalServerErrorException(
'Erro ao buscar nota fiscal',
error instanceof Error ? error.message : 'Erro desconhecido',
);
}
}
async getPermissao(idUsuario: number, codRotina: number): Promise<any> {
const sqlPermissao = `
SELECT PCCONTRO.CODUSUARIO,
PCCONTRO.CODROTINA,
PCCONTRO.ACESSO
FROM PCCONTRO
WHERE PCCONTRO.CODUSUARIO = :idUsuario
AND PCCONTRO.CODROTINA = :codRotina
AND PCCONTRO.ACESSO = 'S'
`;
try {
const result = (await this.databaseService.execute(sqlPermissao, [
idUsuario,
codRotina,
])) as DatabaseResult;
if (!result || !result.rows || result.rows.length === 0) {
throw new NotFoundException(
'Usuario sem permissao para acessar notas fiscais',
);
}
const permissao = result.rows[0] as PermissaoResult;
return {
idUsuario: permissao.CODUSUARIO,
codRotina: permissao.CODROTINA,
acesso: permissao.ACESSO,
};
} catch (error) {
throw new InternalServerErrorException(
'Erro ao buscar permissao',
error instanceof Error ? error.message : 'Erro desconhecido',
);
}
}
async updateRegistro(
chaveNota: string,
codusuario: number,
entrega: EntregaDto,
): Promise<any> {
const sqlUpdate = `
UPDATE PCNFSAID
SET CODFUNCCANHOTO = :codusuario, DTCANHOTO = TRUNC(SYSDATE)
WHERE CHAVENFE = :chaveNota
`;
const sqlInsertEntrega = `INSERT INTO ESTENTREGAS (CODSAIDA, NUMTRANSVENDA, DATA, DOCUMENTORECEBEDOR, NOMERECEBEDOR)
VALUES (0, :NUMTRANSVENDA, SYSDATE, :DOCUMENTO, :RECEBEDOR)`;
const seleInsertEntregaImagem = `INSERT INTO ESTENTREGASIMAGENS (CODSAIDA, NUMTRANSVENDA, TIPO, URL)
VALUES (0,:NUMTRANSVENDA,:TIPO, :URLIMAGEM)`;
try {
const result = (await this.databaseService.execute(sqlUpdate, [
codusuario,
chaveNota,
])) as DatabaseResult;
console.log(result);
// Verificar se algum registro foi afetado
if (result.rowsAffected === 0) {
throw new NotFoundException(
'Nenhum registro encontrado para atualizar',
);
}
const resultEntregas = (await this.databaseService.execute(
sqlInsertEntrega,
[entrega.NUMTRANSVENDA, entrega.DOCUMENTO, entrega.RECEBEDOR],
)) as DatabaseResult;
if (resultEntregas.rowsAffected === 0) {
throw new NotFoundException(
'Nenhum registro de entrega encontrado para inserir',
);
}
for (const imagem of entrega.IMAGENS) {
const resultImagem = (await this.databaseService.execute(
seleInsertEntregaImagem,
[entrega.NUMTRANSVENDA, imagem.TIPO, imagem.URLIMAGEM],
)) as DatabaseResult;
console.log(resultImagem);
if (resultImagem.rowsAffected === 0) {
throw new NotFoundException(
'Nenhum registro de imagem de entrega encontrado para inserir',
);
}
}
return {
message: 'Registro atualizado com sucesso!',
success: true,
rowsAffected: result.rowsAffected,
};
} catch (error) {
if (error instanceof NotFoundException) {
throw error;
}
throw new InternalServerErrorException(
'Erro ao atualizar registro',
error instanceof Error ? error.message : 'Erro desconhecido',
);
}
}
}

25
test/app.e2e-spec.ts Normal file
View File

@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

9
test/jest-e2e.json Normal file
View File

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
tsconfig.build.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}