From 441d8f641173ff590c02b1b7ea10681c1b305a9f Mon Sep 17 00:00:00 2001 From: Brenno Felipe Date: Sun, 2 Nov 2025 21:41:41 -0300 Subject: [PATCH 1/7] feat: "add model Order and migration" --- .../20251102183247_create_order/migration.sql | 19 ++++++++++++++++ prisma/migrations/migration_lock.toml | 4 ++-- prisma/schema.prisma | 22 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20251102183247_create_order/migration.sql diff --git a/prisma/migrations/20251102183247_create_order/migration.sql b/prisma/migrations/20251102183247_create_order/migration.sql new file mode 100644 index 0000000..ba8eb2f --- /dev/null +++ b/prisma/migrations/20251102183247_create_order/migration.sql @@ -0,0 +1,19 @@ +-- CreateEnum +CREATE TYPE "OrderType" AS ENUM ('BUY', 'SELL'); + +-- CreateEnum +CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'EXECUTED', 'CANCELED'); + +-- CreateTable +CREATE TABLE "Order" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "assetId" INTEGER NOT NULL, + "type" "OrderType" NOT NULL, + "status" "OrderStatus" NOT NULL DEFAULT 'PENDING', + "quantity" INTEGER NOT NULL, + "price" DOUBLE PRECISION NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Order_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index 044d57c..fbffa92 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "postgresql" +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3f3c8ee..a24f23a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,4 +21,26 @@ model User { role String @default("user") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt +} + +enum OrderType { + BUY + SELL +} + +enum OrderStatus { + PENDING + EXECUTED + CANCELED +} + +model Order { + id String @id @default(uuid()) + userId String + assetId Int + type OrderType + status OrderStatus @default(PENDING) + quantity Int + price Float + createdAt DateTime @default(now()) } \ No newline at end of file From 190964a8a11d678ee84a6468faad826bea4c2c63 Mon Sep 17 00:00:00 2001 From: Brenno Felipe Date: Sun, 2 Nov 2025 21:51:48 -0300 Subject: [PATCH 2/7] fix: fix in import bcrypt error --- src/auth/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 7f8556e..cb21a1f 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@nestjs/common"; import { JwtService } from "@nestjs/jwt"; import { UsersService } from "../users/users.service"; -import * as bcrypt from "bcrypt"; +import * as bcrypt from "bcryptjs"; import { UnauthorizedException } from "@nestjs/common"; @Injectable() From 173221f6cbcf4ffd053ae3161e9c3cc14ba006cd Mon Sep 17 00:00:00 2001 From: Brenno Felipe Date: Tue, 4 Nov 2025 10:27:54 -0300 Subject: [PATCH 3/7] feat: "implementation CRUD Order" --- src/app.module.ts | 2 + src/orders/dto/create-order.dto.ts | 7 ++ src/orders/dto/update-order.dto.ts | 5 + src/orders/order.controller.ts | 57 +++++++++++ src/orders/order.module.ts | 11 +++ src/orders/order.service.ts | 152 +++++++++++++++++++++++++++++ 6 files changed, 234 insertions(+) create mode 100644 src/orders/dto/create-order.dto.ts create mode 100644 src/orders/dto/update-order.dto.ts create mode 100644 src/orders/order.controller.ts create mode 100644 src/orders/order.module.ts create mode 100644 src/orders/order.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 96db2a6..d687555 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,12 +5,14 @@ import { AssetsModule } from "./assets/assets.module"; import { UsersModule } from "./users/users.module"; import { AuthModule } from "./auth/auth.module"; import { ConfigModule } from "@nestjs/config"; +import { OrdersModule } from "./orders/order.module"; @Module({ imports: [ AssetsModule, UsersModule, AuthModule, + OrdersModule, ConfigModule.forRoot({ isGlobal: true, }), diff --git a/src/orders/dto/create-order.dto.ts b/src/orders/dto/create-order.dto.ts new file mode 100644 index 0000000..11e7f2f --- /dev/null +++ b/src/orders/dto/create-order.dto.ts @@ -0,0 +1,7 @@ +export class CreateOrderDto { + userId: string; + assetId: number; + type: 'BUY' | 'SELL'; + quantity: number; + price: number; + } \ No newline at end of file diff --git a/src/orders/dto/update-order.dto.ts b/src/orders/dto/update-order.dto.ts new file mode 100644 index 0000000..67a3b42 --- /dev/null +++ b/src/orders/dto/update-order.dto.ts @@ -0,0 +1,5 @@ +export class UpdateOrderDto { + status?: 'PENDING' | 'EXECUTED' | 'CANCELED'; + quantity?: number; + price?: number; + } \ No newline at end of file diff --git a/src/orders/order.controller.ts b/src/orders/order.controller.ts new file mode 100644 index 0000000..8937671 --- /dev/null +++ b/src/orders/order.controller.ts @@ -0,0 +1,57 @@ +import { + Controller, + Get, + Post, + Body, + Param, + Put, + Delete, + Query, + } from '@nestjs/common'; + import { OrdersService } from './order.service'; + import { CreateOrderDto } from './dto/create-order.dto'; + import { UpdateOrderDto } from './dto/update-order.dto'; + + @Controller('orders') + export class OrdersController { + constructor(private readonly ordersService: OrdersService) {} + + @Post() + create(@Body() createOrderDto: CreateOrderDto) { + return this.ordersService.create(createOrderDto); + } + + @Get() + findAll(@Query('userId') userId?: string, @Query('assetId') assetId?: string) { + if (userId) { + return this.ordersService.findByUser(userId); + } + if (assetId) { + return this.ordersService.findByAsset(Number(assetId)); + } + return this.ordersService.findAll(); + } + + + + @Get('mock/assets') + getMockAssets() { + return this.ordersService.getMockAssets(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.ordersService.findOne(id); + } + + @Put(':id') + update(@Param('id') id: string, @Body() updateOrderDto: UpdateOrderDto) { + return this.ordersService.update(id, updateOrderDto); + } + + @Delete(':id') + async remove(@Param('id') id: string) { + await this.ordersService.remove(id); + return { message: 'Ordem removida com sucesso' }; + } + } \ No newline at end of file diff --git a/src/orders/order.module.ts b/src/orders/order.module.ts new file mode 100644 index 0000000..e079b0d --- /dev/null +++ b/src/orders/order.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { OrdersController } from './order.controller'; +import { OrdersService } from './order.service'; +import { PrismaService } from '../common/prisma.service'; + +@Module({ + controllers: [OrdersController], + providers: [OrdersService, PrismaService], + exports: [OrdersService], +}) +export class OrdersModule {} \ No newline at end of file diff --git a/src/orders/order.service.ts b/src/orders/order.service.ts new file mode 100644 index 0000000..21354c7 --- /dev/null +++ b/src/orders/order.service.ts @@ -0,0 +1,152 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { PrismaService } from '../common/prisma.service'; +import { CreateOrderDto } from './dto/create-order.dto'; +import { UpdateOrderDto } from './dto/update-order.dto'; + +@Injectable() +export class OrdersService { + constructor(private prisma: PrismaService) {} + + // Assets mockados (até criar o model Asset no Prisma) + private mockAssets = [ + { id: 1, name: 'Tecnologia 11', symbol: 'TEC11' }, + { id: 2, name: 'Finanças V3', symbol: 'FINV3' }, + { id: 3, name: 'Energia Solar', symbol: 'ENER3' }, + { id: 4, name: 'Agronegócio', symbol: 'AGRO4' }, + { id: 5, name: 'Construção', symbol: 'CONS5' }, + ]; + + async create(createOrderDto: CreateOrderDto) { + // Validar se o usuário existe no banco (via Prisma) + const user = await this.prisma.user.findUnique({ + where: { id: createOrderDto.userId }, + }); + if (!user) { + throw new NotFoundException('Usuário não encontrado'); + } + + // Validar se o ativo existe (via mock até criar model Asset) + const asset = this.mockAssets.find(a => a.id === createOrderDto.assetId); + if (!asset) { + throw new NotFoundException('Ativo não encontrado'); + } + + // Validações básicas + if (createOrderDto.quantity <= 0) { + throw new BadRequestException('Quantidade deve ser maior que zero'); + } + + if (createOrderDto.price <= 0) { + throw new BadRequestException('Preço deve ser maior que zero'); + } + + if (createOrderDto.type !== 'BUY' && createOrderDto.type !== 'SELL') { + throw new BadRequestException('Tipo deve ser BUY ou SELL'); + } + + // Criar order no banco usando Prisma (persistência real) + return this.prisma.order.create({ + data: { + userId: createOrderDto.userId, + assetId: createOrderDto.assetId, + type: createOrderDto.type, + quantity: createOrderDto.quantity, + price: createOrderDto.price, + // status tem default 'PENDING' no schema + }, + }); + } + + async findAll() { + return this.prisma.order.findMany({ + orderBy: { + createdAt: 'desc', + }, + }); + } + + async findOne(id: string) { + + const order = await this.prisma.order.findUnique({ + where: { id }, + }); + if (!order) { + throw new NotFoundException('Ordem não encontrada'); + } + return order; + } + + async findByUser(userId: string) { + // Validar se o usuário existe no banco + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + if (!user) { + throw new NotFoundException('Usuário não encontrado'); + } + + + return this.prisma.order.findMany({ + where: { userId }, + orderBy: { + createdAt: 'desc', + }, + }); + } + + async findByAsset(assetId: number) { + // Validar se o ativo existe (mock) + const asset = this.mockAssets.find(a => a.id === assetId); + if (!asset) { + throw new NotFoundException('Ativo não encontrado'); + } + + + return this.prisma.order.findMany({ + where: { assetId }, + orderBy: { + createdAt: 'desc', + }, + }); + } + + async update(id: string, updateOrderDto: UpdateOrderDto) { + const order = await this.findOne(id); + + if (order.status === 'EXECUTED' || order.status === 'CANCELED') { + throw new BadRequestException( + 'Não é possível atualizar uma ordem já executada ou cancelada', + ); + } + + + return this.prisma.order.update({ + where: { id }, + data: updateOrderDto, + }); + } + + async remove(id: string) { + const order = await this.findOne(id); + await this.prisma.order.delete({ + where: { id }, + }); + return order; + } + + // Método auxiliar para listar assets mockados + getMockAssets() { + return this.mockAssets; + } + + // Método auxiliar para listar usuários (via Prisma) + async getUsers() { + return this.prisma.user.findMany({ + select: { + id: true, + email: true, + name: true, + }, + }); + } +} \ No newline at end of file From f6510eb3bfb594cd279b826f32fbc8c503a575c3 Mon Sep 17 00:00:00 2001 From: Brenno Felipe Date: Tue, 4 Nov 2025 10:29:24 -0300 Subject: [PATCH 4/7] feat: implementation test e2e Order --- src/orders/.keep | 1 - test/orders.e2e-spec.ts | 552 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 552 insertions(+), 1 deletion(-) delete mode 100644 src/orders/.keep create mode 100644 test/orders.e2e-spec.ts diff --git a/src/orders/.keep b/src/orders/.keep deleted file mode 100644 index e8f47f0..0000000 --- a/src/orders/.keep +++ /dev/null @@ -1 +0,0 @@ -# Pasta de ordens (orders) diff --git a/test/orders.e2e-spec.ts b/test/orders.e2e-spec.ts new file mode 100644 index 0000000..3ccf779 --- /dev/null +++ b/test/orders.e2e-spec.ts @@ -0,0 +1,552 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { App } from 'supertest/types'; +import { AppModule } from './../src/app.module'; +import { PrismaService } from '../src/common/prisma.service'; + +describe('OrdersController (e2e)', () => { + let app: INestApplication; + let prisma: PrismaService; + let testUserId: string; + let testOrderId: string; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + prisma = moduleFixture.get(PrismaService); + await app.init(); + + + const testUser = await prisma.user.create({ + data: { + email: `test-orders-${Date.now()}@test.com`, + password: 'test123', + name: 'Test User for Orders', + }, + }); + testUserId = testUser.id; + }); + + afterAll(async () => { + try { + // Limpar dados de teste - deletar todas as ordens do usuário de teste primeiro + if (testUserId && prisma) { + // Deletar todas as ordens do usuário de teste + await prisma.order.deleteMany({ + where: { userId: testUserId }, + }).catch(() => {}); + + // Depois deletar o usuário + await prisma.user.delete({ + where: { id: testUserId }, + }).catch(() => {}); + } + } catch (error) { + // Ignorar erros durante limpeza + } finally { + // Sempre fechar a aplicação + if (app) { + await app.close(); + } + } + }); + + describe('POST /orders', () => { + it('deve criar uma ordem de compra com sucesso', () => { + return request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'BUY', + quantity: 10, + price: 100.50, + }) + .expect(201) + .expect((res) => { + expect(res.body).toHaveProperty('id'); + expect(res.body.userId).toBe(testUserId); + expect(res.body.assetId).toBe(1); + expect(res.body.type).toBe('BUY'); + expect(res.body.quantity).toBe(10); + expect(res.body.price).toBe(100.50); + expect(res.body.status).toBe('PENDING'); + testOrderId = res.body.id; + }); + }); + + it('deve criar uma ordem de venda com sucesso', () => { + return request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 2, + type: 'SELL', + quantity: 5, + price: 250.75, + }) + .expect(201) + .expect((res) => { + expect(res.body).toHaveProperty('id'); + expect(res.body.type).toBe('SELL'); + expect(res.body.status).toBe('PENDING'); + }); + }); + + it('deve retornar erro 404 quando usuário não existe', () => { + return request(app.getHttpServer()) + .post('/orders') + .send({ + userId: '00000000-0000-0000-0000-000000000000', + assetId: 1, + type: 'BUY', + quantity: 10, + price: 100.50, + }) + .expect(404) + .expect((res) => { + expect(res.body.message).toContain('Usuário não encontrado'); + }); + }); + + it('deve retornar erro 404 quando ativo não existe', () => { + return request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 999, + type: 'BUY', + quantity: 10, + price: 100.50, + }) + .expect(404) + .expect((res) => { + expect(res.body.message).toContain('Ativo não encontrado'); + }); + }); + + it('deve retornar erro 400 quando quantidade é menor ou igual a zero', () => { + return request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'BUY', + quantity: 0, + price: 100.50, + }) + .expect(400) + .expect((res) => { + expect(res.body.message).toContain('Quantidade deve ser maior que zero'); + }); + }); + + it('deve retornar erro 400 quando quantidade é negativa', () => { + return request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'BUY', + quantity: -5, + price: 100.50, + }) + .expect(400) + .expect((res) => { + expect(res.body.message).toContain('Quantidade deve ser maior que zero'); + }); + }); + + it('deve retornar erro 400 quando preço é menor ou igual a zero', () => { + return request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'BUY', + quantity: 10, + price: 0, + }) + .expect(400) + .expect((res) => { + expect(res.body.message).toContain('Preço deve ser maior que zero'); + }); + }); + + it('deve retornar erro 400 quando preço é negativo', () => { + return request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'BUY', + quantity: 10, + price: -10.50, + }) + .expect(400) + .expect((res) => { + expect(res.body.message).toContain('Preço deve ser maior que zero'); + }); + }); + + it('deve retornar erro 400 quando tipo é inválido', () => { + return request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'INVALID_TYPE', + quantity: 10, + price: 100.50, + }) + .expect(400) + .expect((res) => { + expect(res.body.message).toContain('Tipo deve ser BUY ou SELL'); + }); + }); + }); + + describe('GET /orders', () => { + it('deve retornar todas as ordens', () => { + return request(app.getHttpServer()) + .get('/orders') + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + }); + }); + + it('deve retornar ordens filtradas por userId', async () => { + // Criar uma ordem para garantir que existe pelo menos uma + await request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 3, + type: 'BUY', + quantity: 15, + price: 50.25, + }); + + return request(app.getHttpServer()) + .get(`/orders?userId=${testUserId}`) + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + res.body.forEach((order: any) => { + expect(order.userId).toBe(testUserId); + }); + }); + }); + + it('deve retornar erro 404 quando userId não existe', () => { + return request(app.getHttpServer()) + .get('/orders?userId=00000000-0000-0000-0000-000000000000') + .expect(404) + .expect((res) => { + expect(res.body.message).toContain('Usuário não encontrado'); + }); + }); + + it('deve retornar ordens filtradas por assetId', async () => { + // Criar uma ordem com assetId específico + await request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 4, + type: 'SELL', + quantity: 20, + price: 75.50, + }); + + return request(app.getHttpServer()) + .get('/orders?assetId=4') + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + res.body.forEach((order: any) => { + expect(order.assetId).toBe(4); + }); + }); + }); + + it('deve retornar erro 404 quando assetId não existe', () => { + return request(app.getHttpServer()) + .get('/orders?assetId=999') + .expect(404) + .expect((res) => { + expect(res.body.message).toContain('Ativo não encontrado'); + }); + }); + }); + + describe('GET /orders/mock/assets', () => { + it('deve retornar lista de assets mockados', () => { + return request(app.getHttpServer()) + .get('/orders/mock/assets') + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); + expect(res.body[0]).toHaveProperty('id'); + expect(res.body[0]).toHaveProperty('name'); + expect(res.body[0]).toHaveProperty('symbol'); + }); + }); + }); + + describe('GET /orders/:id', () => { + it('deve retornar uma ordem específica por ID', async () => { + // Criar uma ordem para buscar + const createResponse = await request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 5, + type: 'BUY', + quantity: 30, + price: 200.00, + }); + + const orderId = createResponse.body.id; + + return request(app.getHttpServer()) + .get(`/orders/${orderId}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', orderId); + expect(res.body.userId).toBe(testUserId); + expect(res.body.assetId).toBe(5); + }); + }); + + it('deve retornar erro 404 quando ordem não existe', () => { + return request(app.getHttpServer()) + .get('/orders/00000000-0000-0000-0000-000000000000') + .expect(404) + .expect((res) => { + expect(res.body.message).toContain('Ordem não encontrada'); + }); + }); + }); + + describe('PUT /orders/:id', () => { + let executedOrderId: string; + + beforeAll(async () => { + // Criar uma ordem executada para testar bloqueio de atualização + const executedResponse = await request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 2, + type: 'SELL', + quantity: 5, + price: 250.75, + }); + executedOrderId = executedResponse.body.id; + + // Atualizar para EXECUTED + await prisma.order.update({ + where: { id: executedOrderId }, + data: { status: 'EXECUTED' }, + }); + }); + + it('deve atualizar status da ordem com sucesso', async () => { + // Criar uma nova ordem para este teste + const createResponse = await request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'BUY', + quantity: 10, + price: 100.50, + }); + const orderId = createResponse.body.id; + + return request(app.getHttpServer()) + .put(`/orders/${orderId}`) + .send({ + status: 'EXECUTED', + }) + .expect(200) + .expect((res) => { + expect(res.body.status).toBe('EXECUTED'); + }); + }); + + it('deve atualizar quantidade da ordem com sucesso', async () => { + // Criar uma nova ordem para este teste + const createResponse = await request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'BUY', + quantity: 10, + price: 100.50, + }); + const orderId = createResponse.body.id; + + return request(app.getHttpServer()) + .put(`/orders/${orderId}`) + .send({ + quantity: 25, + }) + .expect(200) + .expect((res) => { + expect(res.body.quantity).toBe(25); + }); + }); + + it('deve atualizar preço da ordem com sucesso', async () => { + // Criar uma nova ordem para este teste + const createResponse = await request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'BUY', + quantity: 10, + price: 100.50, + }); + const orderId = createResponse.body.id; + + return request(app.getHttpServer()) + .put(`/orders/${orderId}`) + .send({ + price: 150.75, + }) + .expect(200) + .expect((res) => { + expect(res.body.price).toBe(150.75); + }); + }); + + it('deve atualizar múltiplos campos da ordem com sucesso', async () => { + // Criar uma nova ordem para este teste + const createResponse = await request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'BUY', + quantity: 10, + price: 100.50, + }); + const orderId = createResponse.body.id; + + return request(app.getHttpServer()) + .put(`/orders/${orderId}`) + .send({ + quantity: 50, + price: 175.25, + status: 'EXECUTED', + }) + .expect(200) + .expect((res) => { + expect(res.body.quantity).toBe(50); + expect(res.body.price).toBe(175.25); + expect(res.body.status).toBe('EXECUTED'); + }); + }); + + it('deve retornar erro 400 quando tenta atualizar ordem executada', () => { + return request(app.getHttpServer()) + .put(`/orders/${executedOrderId}`) + .send({ + quantity: 10, + }) + .expect(400) + .expect((res) => { + expect(res.body.message).toContain( + 'Não é possível atualizar uma ordem já executada ou cancelada', + ); + }); + }); + + it('deve retornar erro 400 quando tenta atualizar ordem cancelada', async () => { + // Criar e cancelar uma ordem + const cancelResponse = await request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 3, + type: 'BUY', + quantity: 10, + price: 100.50, + }); + + const canceledOrderId = cancelResponse.body.id; + + await prisma.order.update({ + where: { id: canceledOrderId }, + data: { status: 'CANCELED' }, + }); + + return request(app.getHttpServer()) + .put(`/orders/${canceledOrderId}`) + .send({ + quantity: 20, + }) + .expect(400) + .expect((res) => { + expect(res.body.message).toContain( + 'Não é possível atualizar uma ordem já executada ou cancelada', + ); + }); + }); + + it('deve retornar erro 404 quando ordem não existe', () => { + return request(app.getHttpServer()) + .put('/orders/00000000-0000-0000-0000-000000000000') + .send({ + status: 'EXECUTED', + }) + .expect(404) + .expect((res) => { + expect(res.body.message).toContain('Ordem não encontrada'); + }); + }); + }); + + describe('DELETE /orders/:id', () => { + it('deve remover uma ordem com sucesso', async () => { + // Criar uma ordem para remover + const createResponse = await request(app.getHttpServer()) + .post('/orders') + .send({ + userId: testUserId, + assetId: 1, + type: 'BUY', + quantity: 10, + price: 100.50, + }); + + const orderId = createResponse.body.id; + + return request(app.getHttpServer()) + .delete(`/orders/${orderId}`) + .expect(200) + .expect((res) => { + expect(res.body.message).toContain('removida com sucesso'); + }); + }); + + it('deve retornar erro 404 quando ordem não existe', () => { + return request(app.getHttpServer()) + .delete('/orders/00000000-0000-0000-0000-000000000000') + .expect(404) + .expect((res) => { + expect(res.body.message).toContain('Ordem não encontrada'); + }); + }); + }); +}); From b2e0c85acbb9336fa34c4b444a921936f511c5d5 Mon Sep 17 00:00:00 2001 From: Brenno Felipe Date: Tue, 25 Nov 2025 20:24:28 -0300 Subject: [PATCH 5/7] docs: add swagger in create-order --- src/orders/dto/create-order.dto.ts | 39 +++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/orders/dto/create-order.dto.ts b/src/orders/dto/create-order.dto.ts index 11e7f2f..adeef91 100644 --- a/src/orders/dto/create-order.dto.ts +++ b/src/orders/dto/create-order.dto.ts @@ -1,7 +1,34 @@ +import { ApiProperty } from '@nestjs/swagger'; + export class CreateOrderDto { - userId: string; - assetId: number; - type: 'BUY' | 'SELL'; - quantity: number; - price: number; - } \ No newline at end of file + @ApiProperty({ + description: 'ID of the user placing the order.', + example: 'a1b2c3d4-e5f6-7890-ab12-cd34ef567890', + }) + userId: string; + + @ApiProperty({ + description: 'ID of the asset involved in the order.', + example: 1, + }) + assetId: number; + + @ApiProperty({ + description: 'Type of the order: BUY or SELL.', + enum: ['BUY', 'SELL'], + example: 'BUY', + }) + type: 'BUY' | 'SELL'; + + @ApiProperty({ + description: 'Amount of the asset to be traded. Must be greater than zero.', + example: 10, + }) + quantity: number; + + @ApiProperty({ + description: 'Price per unit of the asset at the moment the order is placed.', + example: 150.75, + }) + price: number; +} From 3f27aef9357e2c1d87bd7acf7160cfb0bc4eea42 Mon Sep 17 00:00:00 2001 From: Brenno Felipe Date: Tue, 25 Nov 2025 20:25:15 -0300 Subject: [PATCH 6/7] docs: add swagger in updade-order.dto --- src/orders/dto/update-order.dto.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/orders/dto/update-order.dto.ts b/src/orders/dto/update-order.dto.ts index 67a3b42..4dedd43 100644 --- a/src/orders/dto/update-order.dto.ts +++ b/src/orders/dto/update-order.dto.ts @@ -1,5 +1,22 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; + export class UpdateOrderDto { - status?: 'PENDING' | 'EXECUTED' | 'CANCELED'; - quantity?: number; - price?: number; - } \ No newline at end of file + @ApiPropertyOptional({ + description: 'Updated status of the order', + enum: ['PENDING', 'EXECUTED', 'CANCELED'], + example: 'EXECUTED', + }) + status?: 'PENDING' | 'EXECUTED' | 'CANCELED'; + + @ApiPropertyOptional({ + description: 'Updated quantity of the order. Must be greater than zero.', + example: 15, + }) + quantity?: number; + + @ApiPropertyOptional({ + description: 'Updated price of the order. Must be greater than zero.', + example: 120.50, + }) + price?: number; +} From f5379baa447c0088fa949238c5012a22c8ee9023 Mon Sep 17 00:00:00 2001 From: Brenno Felipe Date: Thu, 27 Nov 2025 00:09:50 -0300 Subject: [PATCH 7/7] feat: add config swagger --- package.json | 4 +- pnpm-lock.yaml | 107 ++++ .../migrations/20251127000653_/migration.sql | 22 + prisma/migrations/migration_lock.toml | 4 +- prisma/schema.prisma | 3 + src/main.ts | 18 +- src/orders/dto/create-order.dto.ts | 16 +- src/orders/dto/update-order.dto.ts | 44 +- src/orders/order.controller.ts | 143 +++-- src/orders/order.service.ts | 159 ++--- test/orders.e2e-spec.ts | 552 ------------------ 11 files changed, 351 insertions(+), 721 deletions(-) create mode 100644 prisma/migrations/20251127000653_/migration.sql delete mode 100644 test/orders.e2e-spec.ts diff --git a/package.json b/package.json index 62800be..2a42216 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.2.3", "@prisma/client": "^6.16.2", "bcrypt": "^6.0.0", "bcryptjs": "^3.0.2", @@ -30,7 +31,8 @@ "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@nestjs/cli": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05a36ca..db790b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@nestjs/platform-express': specifier: ^11.0.1 version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + '@nestjs/swagger': + specifier: ^11.2.3 + version: 11.2.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) '@prisma/client': specifier: ^6.16.2 version: 6.16.2(prisma@6.16.2(typescript@5.9.2))(typescript@5.9.2) @@ -50,6 +53,9 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.2 + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@5.1.0) devDependencies: '@nestjs/cli': specifier: ^11.0.0 @@ -584,6 +590,9 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -642,6 +651,19 @@ packages: peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/mapped-types@2.1.0': + resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/passport@10.0.3': resolution: {integrity: sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==} peerDependencies: @@ -659,6 +681,23 @@ packages: peerDependencies: typescript: '>=4.8.2' + '@nestjs/swagger@11.2.3': + resolution: {integrity: sha512-a0xFfjeqk69uHIUpP8u0ryn4cKuHdra2Ug96L858i0N200Hxho+n3j+TlQXyOF4EstLSGjTfxI1Xb2E1lUxeNg==} + peerDependencies: + '@fastify/static': ^8.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/testing@11.1.6': resolution: {integrity: sha512-srYzzDNxGvVCe1j0SpTS9/ix75PKt6Sn6iMaH1rpJ6nj2g8vwNrhK0CoJJXvpCYgrnI+2WES2pprYnq8rAMYHA==} peerDependencies: @@ -722,6 +761,9 @@ packages: '@prisma/get-platform@6.16.2': resolution: {integrity: sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==} + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@sinclair/typebox@0.34.41': resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} @@ -2023,6 +2065,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2374,6 +2420,9 @@ packages: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2759,6 +2808,18 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swagger-ui-dist@5.30.2: + resolution: {integrity: sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==} + + swagger-ui-dist@5.30.3: + resolution: {integrity: sha512-giQl7/ToPxCqnUAx2wpnSnDNGZtGzw1LyUw6ZitIpTmdrvpxKFY/94v1hihm0zYNpgp1/VY0jTDk//R0BBgnRQ==} + + swagger-ui-express@5.0.1: + resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -3690,6 +3751,8 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@microsoft/tsdoc@0.16.0': {} + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.5.0 @@ -3767,6 +3830,14 @@ snapshots: '@types/jsonwebtoken': 9.0.5 jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.2 + '@nestjs/passport@10.0.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -3806,6 +3877,21 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/swagger@11.2.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + js-yaml: 4.1.1 + lodash: 4.17.21 + path-to-regexp: 8.3.0 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.30.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.2 + '@nestjs/testing@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-express@11.1.6)': dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -3864,6 +3950,8 @@ snapshots: dependencies: '@prisma/debug': 6.16.2 + '@scarf/scarf@1.4.0': {} + '@sinclair/typebox@0.34.41': {} '@sinonjs/commons@3.0.1': @@ -5396,6 +5484,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} json-parse-even-better-errors@2.3.1: {} @@ -5700,6 +5792,8 @@ snapshots: path-to-regexp@8.2.0: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -6082,6 +6176,19 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swagger-ui-dist@5.30.2: + dependencies: + '@scarf/scarf': 1.4.0 + + swagger-ui-dist@5.30.3: + dependencies: + '@scarf/scarf': 1.4.0 + + swagger-ui-express@5.0.1(express@5.1.0): + dependencies: + express: 5.1.0 + swagger-ui-dist: 5.30.3 + symbol-observable@4.0.0: {} synckit@0.11.11: diff --git a/prisma/migrations/20251127000653_/migration.sql b/prisma/migrations/20251127000653_/migration.sql new file mode 100644 index 0000000..c1f4a01 --- /dev/null +++ b/prisma/migrations/20251127000653_/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - You are about to drop the `Order` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "public"."Order"; + +-- CreateTable +CREATE TABLE "public"."orders" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "assetId" INTEGER NOT NULL, + "type" "public"."OrderType" NOT NULL, + "status" "public"."OrderStatus" NOT NULL DEFAULT 'PENDING', + "quantity" INTEGER NOT NULL, + "price" DOUBLE PRECISION NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "orders_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index fbffa92..044d57c 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a24f23a..091b87d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -43,4 +43,7 @@ model Order { quantity Int price Float createdAt DateTime @default(now()) + + @@map("orders") + } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 8ccda45..16695d7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,23 @@ 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); - await app.listen(process.env.PORT ?? 3000); + + + + const config = new DocumentBuilder() + .setTitle('Orders API') + .setDescription('API documentation for the Orders system') + .setVersion('1.0') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + + console.log('Swagger running at http://localhost:3000/api'); app.enableCors({ origin: process.env.CORS_ORIGIN || "*", @@ -19,5 +32,8 @@ async function bootstrap() { transform: true, // converts plain JavaScript objects into instances of their corresponding DTO classes }) ); + + await app.listen(process.env.PORT ?? 3000); + } bootstrap(); diff --git a/src/orders/dto/create-order.dto.ts b/src/orders/dto/create-order.dto.ts index adeef91..2a5d300 100644 --- a/src/orders/dto/create-order.dto.ts +++ b/src/orders/dto/create-order.dto.ts @@ -1,16 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNumber, IsPositive } from 'class-validator'; export class CreateOrderDto { - @ApiProperty({ - description: 'ID of the user placing the order.', - example: 'a1b2c3d4-e5f6-7890-ab12-cd34ef567890', - }) - userId: string; - @ApiProperty({ description: 'ID of the asset involved in the order.', example: 1, }) + @IsNumber() + @IsPositive() assetId: number; @ApiProperty({ @@ -18,17 +15,24 @@ export class CreateOrderDto { enum: ['BUY', 'SELL'], example: 'BUY', }) + @IsEnum(['BUY', 'SELL'], { + message: 'type must be BUY or SELL', + }) type: 'BUY' | 'SELL'; @ApiProperty({ description: 'Amount of the asset to be traded. Must be greater than zero.', example: 10, }) + @IsNumber() + @IsPositive() quantity: number; @ApiProperty({ description: 'Price per unit of the asset at the moment the order is placed.', example: 150.75, }) + @IsNumber() + @IsPositive() price: number; } diff --git a/src/orders/dto/update-order.dto.ts b/src/orders/dto/update-order.dto.ts index 4dedd43..ab6c34d 100644 --- a/src/orders/dto/update-order.dto.ts +++ b/src/orders/dto/update-order.dto.ts @@ -1,22 +1,44 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsNumber, IsOptional, IsPositive } from 'class-validator'; + +export enum OrderStatusEnum { + PENDING = 'PENDING', + EXECUTED = 'EXECUTED', + CANCELED = 'CANCELED', +} export class UpdateOrderDto { - @ApiPropertyOptional({ - description: 'Updated status of the order', - enum: ['PENDING', 'EXECUTED', 'CANCELED'], - example: 'EXECUTED', - }) - status?: 'PENDING' | 'EXECUTED' | 'CANCELED'; + @ApiPropertyOptional() + @IsOptional() + userId?: string; + + @ApiPropertyOptional() + @IsOptional() + assetId?: number; @ApiPropertyOptional({ - description: 'Updated quantity of the order. Must be greater than zero.', - example: 15, + enum: ['BUY', 'SELL'], }) + @IsOptional() + @IsEnum(['BUY', 'SELL']) + type?: 'BUY' | 'SELL'; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + @IsPositive() quantity?: number; + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + @IsPositive() + price?: number; + @ApiPropertyOptional({ - description: 'Updated price of the order. Must be greater than zero.', - example: 120.50, + enum: OrderStatusEnum, }) - price?: number; + @IsOptional() + @IsEnum(OrderStatusEnum) + status?: OrderStatusEnum; } diff --git a/src/orders/order.controller.ts b/src/orders/order.controller.ts index 8937671..bc14a0d 100644 --- a/src/orders/order.controller.ts +++ b/src/orders/order.controller.ts @@ -1,57 +1,94 @@ import { - Controller, - Get, - Post, - Body, - Param, - Put, - Delete, - Query, - } from '@nestjs/common'; - import { OrdersService } from './order.service'; - import { CreateOrderDto } from './dto/create-order.dto'; - import { UpdateOrderDto } from './dto/update-order.dto'; - - @Controller('orders') - export class OrdersController { - constructor(private readonly ordersService: OrdersService) {} - - @Post() - create(@Body() createOrderDto: CreateOrderDto) { - return this.ordersService.create(createOrderDto); - } - - @Get() - findAll(@Query('userId') userId?: string, @Query('assetId') assetId?: string) { - if (userId) { - return this.ordersService.findByUser(userId); - } - if (assetId) { - return this.ordersService.findByAsset(Number(assetId)); - } - return this.ordersService.findAll(); - } - - - - @Get('mock/assets') - getMockAssets() { - return this.ordersService.getMockAssets(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.ordersService.findOne(id); - } - - @Put(':id') - update(@Param('id') id: string, @Body() updateOrderDto: UpdateOrderDto) { - return this.ordersService.update(id, updateOrderDto); - } - - @Delete(':id') + Controller, + Get, + Post, + Body, + Param, + Put, + Delete, + Query, +} from '@nestjs/common'; +import { OrdersService } from './order.service'; +import { CreateOrderDto } from './dto/create-order.dto'; +import { UpdateOrderDto } from './dto/update-order.dto'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiQuery, + ApiParam, + ApiBody, +} from '@nestjs/swagger'; + +@ApiTags('Orders') +@Controller('orders') +export class OrdersController { + constructor(private readonly ordersService: OrdersService) {} + + @Post() + @ApiOperation({ + summary: 'Create a new order (mocked userId: u1)', + }) + @ApiBody({ type: CreateOrderDto }) + @ApiResponse({ status: 201, description: 'Order created successfully' }) + @ApiResponse({ status: 400, description: 'Missing or invalid fields' }) + create(@Body() dto: CreateOrderDto) { + const mockedUserId = 'u1'; // usuário mockado real + return this.ordersService.create(mockedUserId, dto); + } + + @Get('user/:userId') + @ApiOperation({ summary: 'Find all orders by userId' }) + @ApiParam({ + name: 'userId', + description: 'User ID to filter orders', + example: 'u1', + }) + findByUser(@Param('userId') userId: string) { + return this.ordersService.findByUser(userId); + } + + @Get() + @ApiOperation({ summary: 'Find all orders or filter by userId/assetId' }) + @ApiQuery({ name: 'userId', required: false }) + @ApiQuery({ name: 'assetId', required: false }) + findAll( + @Query('userId') userId?: string, + @Query('assetId') assetId?: string, + ) { + if (userId) return this.ordersService.findByUser(userId); + if (assetId) return this.ordersService.findByAsset(Number(assetId)); + return this.ordersService.findAll(); + } + + @Get('mock/assets') + @ApiOperation({ summary: 'Returns mocked assets for testing' }) + getMockAssets() { + return this.ordersService.getMockAssets(); + } + + @Get(':id') + @ApiOperation({ summary: 'Find an order by ID' }) + @ApiParam({ + name: 'id', + description: 'Order ID', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + findOne(@Param('id') id: string) { + return this.ordersService.findOne(id); + } + + @Put(':id') + @ApiOperation({ summary: 'Update an order by ID' }) + @ApiBody({ type: UpdateOrderDto }) + update(@Param('id') id: string, @Body() dto: UpdateOrderDto) { + return this.ordersService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: 'Remove an order by ID' }) async remove(@Param('id') id: string) { await this.ordersService.remove(id); - return { message: 'Ordem removida com sucesso' }; + return { message: 'Order removed successfully' }; } - } \ No newline at end of file +} diff --git a/src/orders/order.service.ts b/src/orders/order.service.ts index 21354c7..8f00dd5 100644 --- a/src/orders/order.service.ts +++ b/src/orders/order.service.ts @@ -3,150 +3,119 @@ import { PrismaService } from '../common/prisma.service'; import { CreateOrderDto } from './dto/create-order.dto'; import { UpdateOrderDto } from './dto/update-order.dto'; + +const mockUsers = [ + { id: 'u1', name: 'João Silva', email: 'joao@example.com' }, + { id: 'u2', name: 'Maria Oliveira', email: 'maria@example.com' }, + { id: 'u3', name: 'Carlos Pereira', email: 'carlos@example.com' }, +]; + + +const mockAssets = [ + { id: 1, name: 'Tecnologia 11', symbol: 'TEC11' }, + { id: 2, name: 'Finanças V3', symbol: 'FINV3' }, + { id: 3, name: 'Energia Solar', symbol: 'ENER3' }, + { id: 4, name: 'Agronegócio', symbol: 'AGRO4' }, + { id: 5, name: 'Construção', symbol: 'CONS5' }, +]; + + +const DEFAULT_USER_ID = 'u1'; + @Injectable() export class OrdersService { constructor(private prisma: PrismaService) {} - // Assets mockados (até criar o model Asset no Prisma) - private mockAssets = [ - { id: 1, name: 'Tecnologia 11', symbol: 'TEC11' }, - { id: 2, name: 'Finanças V3', symbol: 'FINV3' }, - { id: 3, name: 'Energia Solar', symbol: 'ENER3' }, - { id: 4, name: 'Agronegócio', symbol: 'AGRO4' }, - { id: 5, name: 'Construção', symbol: 'CONS5' }, - ]; - - async create(createOrderDto: CreateOrderDto) { - // Validar se o usuário existe no banco (via Prisma) - const user = await this.prisma.user.findUnique({ - where: { id: createOrderDto.userId }, - }); - if (!user) { - throw new NotFoundException('Usuário não encontrado'); - } + async create(userId: string, dto: CreateOrderDto) { + const { assetId, quantity, price, type } = dto; - // Validar se o ativo existe (via mock até criar model Asset) - const asset = this.mockAssets.find(a => a.id === createOrderDto.assetId); - if (!asset) { - throw new NotFoundException('Ativo não encontrado'); - } + + const resolvedUserId = userId || DEFAULT_USER_ID; - // Validações básicas - if (createOrderDto.quantity <= 0) { - throw new BadRequestException('Quantidade deve ser maior que zero'); - } + + const user = mockUsers.find(u => u.id === resolvedUserId); + if (!user) throw new NotFoundException('User not found (mock).'); - if (createOrderDto.price <= 0) { - throw new BadRequestException('Preço deve ser maior que zero'); - } + + const asset = mockAssets.find(a => a.id === assetId); + if (!asset) throw new NotFoundException('Asset not found (mock).'); - if (createOrderDto.type !== 'BUY' && createOrderDto.type !== 'SELL') { - throw new BadRequestException('Tipo deve ser BUY ou SELL'); - } + + if (quantity <= 0) throw new BadRequestException('Quantity must be greater than zero.'); + if (price <= 0) throw new BadRequestException('Price must be greater than zero.'); + if (!['BUY', 'SELL'].includes(type)) + throw new BadRequestException('Type must be BUY or SELL.'); - // Criar order no banco usando Prisma (persistência real) + return this.prisma.order.create({ data: { - userId: createOrderDto.userId, - assetId: createOrderDto.assetId, - type: createOrderDto.type, - quantity: createOrderDto.quantity, - price: createOrderDto.price, - // status tem default 'PENDING' no schema + userId: resolvedUserId, + assetId, + quantity, + price, + type, + status: 'PENDING', }, }); } async findAll() { return this.prisma.order.findMany({ - orderBy: { - createdAt: 'desc', - }, + orderBy: { createdAt: 'desc' }, }); } async findOne(id: string) { - - const order = await this.prisma.order.findUnique({ - where: { id }, - }); - if (!order) { - throw new NotFoundException('Ordem não encontrada'); - } + const order = await this.prisma.order.findUnique({ where: { id } }); + if (!order) throw new NotFoundException('Order not found.'); return order; } async findByUser(userId: string) { - // Validar se o usuário existe no banco - const user = await this.prisma.user.findUnique({ - where: { id: userId }, - }); - if (!user) { - throw new NotFoundException('Usuário não encontrado'); - } + const resolvedUserId = userId || DEFAULT_USER_ID; + + const user = mockUsers.find(u => u.id === resolvedUserId); + if (!user) throw new NotFoundException('User not found (mock).'); - return this.prisma.order.findMany({ - where: { userId }, - orderBy: { - createdAt: 'desc', - }, + where: { userId: resolvedUserId }, + orderBy: { createdAt: 'desc' }, }); } async findByAsset(assetId: number) { - // Validar se o ativo existe (mock) - const asset = this.mockAssets.find(a => a.id === assetId); - if (!asset) { - throw new NotFoundException('Ativo não encontrado'); - } - + const asset = mockAssets.find(a => a.id === assetId); + if (!asset) throw new NotFoundException('Asset not found (mock).'); return this.prisma.order.findMany({ where: { assetId }, - orderBy: { - createdAt: 'desc', - }, + orderBy: { createdAt: 'desc' }, }); } - async update(id: string, updateOrderDto: UpdateOrderDto) { - const order = await this.findOne(id); + async update(id: string, dto: UpdateOrderDto) { + const existing = await this.findOne(id); - if (order.status === 'EXECUTED' || order.status === 'CANCELED') { - throw new BadRequestException( - 'Não é possível atualizar uma ordem já executada ou cancelada', - ); + if (existing.status === 'EXECUTED' || existing.status === 'CANCELED') { + throw new BadRequestException('Cannot update executed or canceled order.'); } - return this.prisma.order.update({ where: { id }, - data: updateOrderDto, + data: dto, }); } async remove(id: string) { - const order = await this.findOne(id); - await this.prisma.order.delete({ - where: { id }, - }); - return order; + await this.findOne(id); + return this.prisma.order.delete({ where: { id } }); } - // Método auxiliar para listar assets mockados getMockAssets() { - return this.mockAssets; + return mockAssets; } - // Método auxiliar para listar usuários (via Prisma) - async getUsers() { - return this.prisma.user.findMany({ - select: { - id: true, - email: true, - name: true, - }, - }); + getMockUsers() { + return mockUsers; } -} \ No newline at end of file +} diff --git a/test/orders.e2e-spec.ts b/test/orders.e2e-spec.ts deleted file mode 100644 index 3ccf779..0000000 --- a/test/orders.e2e-spec.ts +++ /dev/null @@ -1,552 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import request from 'supertest'; -import { App } from 'supertest/types'; -import { AppModule } from './../src/app.module'; -import { PrismaService } from '../src/common/prisma.service'; - -describe('OrdersController (e2e)', () => { - let app: INestApplication; - let prisma: PrismaService; - let testUserId: string; - let testOrderId: string; - - beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - prisma = moduleFixture.get(PrismaService); - await app.init(); - - - const testUser = await prisma.user.create({ - data: { - email: `test-orders-${Date.now()}@test.com`, - password: 'test123', - name: 'Test User for Orders', - }, - }); - testUserId = testUser.id; - }); - - afterAll(async () => { - try { - // Limpar dados de teste - deletar todas as ordens do usuário de teste primeiro - if (testUserId && prisma) { - // Deletar todas as ordens do usuário de teste - await prisma.order.deleteMany({ - where: { userId: testUserId }, - }).catch(() => {}); - - // Depois deletar o usuário - await prisma.user.delete({ - where: { id: testUserId }, - }).catch(() => {}); - } - } catch (error) { - // Ignorar erros durante limpeza - } finally { - // Sempre fechar a aplicação - if (app) { - await app.close(); - } - } - }); - - describe('POST /orders', () => { - it('deve criar uma ordem de compra com sucesso', () => { - return request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'BUY', - quantity: 10, - price: 100.50, - }) - .expect(201) - .expect((res) => { - expect(res.body).toHaveProperty('id'); - expect(res.body.userId).toBe(testUserId); - expect(res.body.assetId).toBe(1); - expect(res.body.type).toBe('BUY'); - expect(res.body.quantity).toBe(10); - expect(res.body.price).toBe(100.50); - expect(res.body.status).toBe('PENDING'); - testOrderId = res.body.id; - }); - }); - - it('deve criar uma ordem de venda com sucesso', () => { - return request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 2, - type: 'SELL', - quantity: 5, - price: 250.75, - }) - .expect(201) - .expect((res) => { - expect(res.body).toHaveProperty('id'); - expect(res.body.type).toBe('SELL'); - expect(res.body.status).toBe('PENDING'); - }); - }); - - it('deve retornar erro 404 quando usuário não existe', () => { - return request(app.getHttpServer()) - .post('/orders') - .send({ - userId: '00000000-0000-0000-0000-000000000000', - assetId: 1, - type: 'BUY', - quantity: 10, - price: 100.50, - }) - .expect(404) - .expect((res) => { - expect(res.body.message).toContain('Usuário não encontrado'); - }); - }); - - it('deve retornar erro 404 quando ativo não existe', () => { - return request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 999, - type: 'BUY', - quantity: 10, - price: 100.50, - }) - .expect(404) - .expect((res) => { - expect(res.body.message).toContain('Ativo não encontrado'); - }); - }); - - it('deve retornar erro 400 quando quantidade é menor ou igual a zero', () => { - return request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'BUY', - quantity: 0, - price: 100.50, - }) - .expect(400) - .expect((res) => { - expect(res.body.message).toContain('Quantidade deve ser maior que zero'); - }); - }); - - it('deve retornar erro 400 quando quantidade é negativa', () => { - return request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'BUY', - quantity: -5, - price: 100.50, - }) - .expect(400) - .expect((res) => { - expect(res.body.message).toContain('Quantidade deve ser maior que zero'); - }); - }); - - it('deve retornar erro 400 quando preço é menor ou igual a zero', () => { - return request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'BUY', - quantity: 10, - price: 0, - }) - .expect(400) - .expect((res) => { - expect(res.body.message).toContain('Preço deve ser maior que zero'); - }); - }); - - it('deve retornar erro 400 quando preço é negativo', () => { - return request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'BUY', - quantity: 10, - price: -10.50, - }) - .expect(400) - .expect((res) => { - expect(res.body.message).toContain('Preço deve ser maior que zero'); - }); - }); - - it('deve retornar erro 400 quando tipo é inválido', () => { - return request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'INVALID_TYPE', - quantity: 10, - price: 100.50, - }) - .expect(400) - .expect((res) => { - expect(res.body.message).toContain('Tipo deve ser BUY ou SELL'); - }); - }); - }); - - describe('GET /orders', () => { - it('deve retornar todas as ordens', () => { - return request(app.getHttpServer()) - .get('/orders') - .expect(200) - .expect((res) => { - expect(Array.isArray(res.body)).toBe(true); - }); - }); - - it('deve retornar ordens filtradas por userId', async () => { - // Criar uma ordem para garantir que existe pelo menos uma - await request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 3, - type: 'BUY', - quantity: 15, - price: 50.25, - }); - - return request(app.getHttpServer()) - .get(`/orders?userId=${testUserId}`) - .expect(200) - .expect((res) => { - expect(Array.isArray(res.body)).toBe(true); - res.body.forEach((order: any) => { - expect(order.userId).toBe(testUserId); - }); - }); - }); - - it('deve retornar erro 404 quando userId não existe', () => { - return request(app.getHttpServer()) - .get('/orders?userId=00000000-0000-0000-0000-000000000000') - .expect(404) - .expect((res) => { - expect(res.body.message).toContain('Usuário não encontrado'); - }); - }); - - it('deve retornar ordens filtradas por assetId', async () => { - // Criar uma ordem com assetId específico - await request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 4, - type: 'SELL', - quantity: 20, - price: 75.50, - }); - - return request(app.getHttpServer()) - .get('/orders?assetId=4') - .expect(200) - .expect((res) => { - expect(Array.isArray(res.body)).toBe(true); - res.body.forEach((order: any) => { - expect(order.assetId).toBe(4); - }); - }); - }); - - it('deve retornar erro 404 quando assetId não existe', () => { - return request(app.getHttpServer()) - .get('/orders?assetId=999') - .expect(404) - .expect((res) => { - expect(res.body.message).toContain('Ativo não encontrado'); - }); - }); - }); - - describe('GET /orders/mock/assets', () => { - it('deve retornar lista de assets mockados', () => { - return request(app.getHttpServer()) - .get('/orders/mock/assets') - .expect(200) - .expect((res) => { - expect(Array.isArray(res.body)).toBe(true); - expect(res.body.length).toBeGreaterThan(0); - expect(res.body[0]).toHaveProperty('id'); - expect(res.body[0]).toHaveProperty('name'); - expect(res.body[0]).toHaveProperty('symbol'); - }); - }); - }); - - describe('GET /orders/:id', () => { - it('deve retornar uma ordem específica por ID', async () => { - // Criar uma ordem para buscar - const createResponse = await request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 5, - type: 'BUY', - quantity: 30, - price: 200.00, - }); - - const orderId = createResponse.body.id; - - return request(app.getHttpServer()) - .get(`/orders/${orderId}`) - .expect(200) - .expect((res) => { - expect(res.body).toHaveProperty('id', orderId); - expect(res.body.userId).toBe(testUserId); - expect(res.body.assetId).toBe(5); - }); - }); - - it('deve retornar erro 404 quando ordem não existe', () => { - return request(app.getHttpServer()) - .get('/orders/00000000-0000-0000-0000-000000000000') - .expect(404) - .expect((res) => { - expect(res.body.message).toContain('Ordem não encontrada'); - }); - }); - }); - - describe('PUT /orders/:id', () => { - let executedOrderId: string; - - beforeAll(async () => { - // Criar uma ordem executada para testar bloqueio de atualização - const executedResponse = await request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 2, - type: 'SELL', - quantity: 5, - price: 250.75, - }); - executedOrderId = executedResponse.body.id; - - // Atualizar para EXECUTED - await prisma.order.update({ - where: { id: executedOrderId }, - data: { status: 'EXECUTED' }, - }); - }); - - it('deve atualizar status da ordem com sucesso', async () => { - // Criar uma nova ordem para este teste - const createResponse = await request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'BUY', - quantity: 10, - price: 100.50, - }); - const orderId = createResponse.body.id; - - return request(app.getHttpServer()) - .put(`/orders/${orderId}`) - .send({ - status: 'EXECUTED', - }) - .expect(200) - .expect((res) => { - expect(res.body.status).toBe('EXECUTED'); - }); - }); - - it('deve atualizar quantidade da ordem com sucesso', async () => { - // Criar uma nova ordem para este teste - const createResponse = await request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'BUY', - quantity: 10, - price: 100.50, - }); - const orderId = createResponse.body.id; - - return request(app.getHttpServer()) - .put(`/orders/${orderId}`) - .send({ - quantity: 25, - }) - .expect(200) - .expect((res) => { - expect(res.body.quantity).toBe(25); - }); - }); - - it('deve atualizar preço da ordem com sucesso', async () => { - // Criar uma nova ordem para este teste - const createResponse = await request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'BUY', - quantity: 10, - price: 100.50, - }); - const orderId = createResponse.body.id; - - return request(app.getHttpServer()) - .put(`/orders/${orderId}`) - .send({ - price: 150.75, - }) - .expect(200) - .expect((res) => { - expect(res.body.price).toBe(150.75); - }); - }); - - it('deve atualizar múltiplos campos da ordem com sucesso', async () => { - // Criar uma nova ordem para este teste - const createResponse = await request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'BUY', - quantity: 10, - price: 100.50, - }); - const orderId = createResponse.body.id; - - return request(app.getHttpServer()) - .put(`/orders/${orderId}`) - .send({ - quantity: 50, - price: 175.25, - status: 'EXECUTED', - }) - .expect(200) - .expect((res) => { - expect(res.body.quantity).toBe(50); - expect(res.body.price).toBe(175.25); - expect(res.body.status).toBe('EXECUTED'); - }); - }); - - it('deve retornar erro 400 quando tenta atualizar ordem executada', () => { - return request(app.getHttpServer()) - .put(`/orders/${executedOrderId}`) - .send({ - quantity: 10, - }) - .expect(400) - .expect((res) => { - expect(res.body.message).toContain( - 'Não é possível atualizar uma ordem já executada ou cancelada', - ); - }); - }); - - it('deve retornar erro 400 quando tenta atualizar ordem cancelada', async () => { - // Criar e cancelar uma ordem - const cancelResponse = await request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 3, - type: 'BUY', - quantity: 10, - price: 100.50, - }); - - const canceledOrderId = cancelResponse.body.id; - - await prisma.order.update({ - where: { id: canceledOrderId }, - data: { status: 'CANCELED' }, - }); - - return request(app.getHttpServer()) - .put(`/orders/${canceledOrderId}`) - .send({ - quantity: 20, - }) - .expect(400) - .expect((res) => { - expect(res.body.message).toContain( - 'Não é possível atualizar uma ordem já executada ou cancelada', - ); - }); - }); - - it('deve retornar erro 404 quando ordem não existe', () => { - return request(app.getHttpServer()) - .put('/orders/00000000-0000-0000-0000-000000000000') - .send({ - status: 'EXECUTED', - }) - .expect(404) - .expect((res) => { - expect(res.body.message).toContain('Ordem não encontrada'); - }); - }); - }); - - describe('DELETE /orders/:id', () => { - it('deve remover uma ordem com sucesso', async () => { - // Criar uma ordem para remover - const createResponse = await request(app.getHttpServer()) - .post('/orders') - .send({ - userId: testUserId, - assetId: 1, - type: 'BUY', - quantity: 10, - price: 100.50, - }); - - const orderId = createResponse.body.id; - - return request(app.getHttpServer()) - .delete(`/orders/${orderId}`) - .expect(200) - .expect((res) => { - expect(res.body.message).toContain('removida com sucesso'); - }); - }); - - it('deve retornar erro 404 quando ordem não existe', () => { - return request(app.getHttpServer()) - .delete('/orders/00000000-0000-0000-0000-000000000000') - .expect(404) - .expect((res) => { - expect(res.body.message).toContain('Ordem não encontrada'); - }); - }); - }); -});