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/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/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/schema.prisma b/prisma/schema.prisma index 3f3c8ee..091b87d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,4 +21,29 @@ 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()) + + @@map("orders") + } \ No newline at end of file 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/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() 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/.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/src/orders/dto/create-order.dto.ts b/src/orders/dto/create-order.dto.ts new file mode 100644 index 0000000..2a5d300 --- /dev/null +++ b/src/orders/dto/create-order.dto.ts @@ -0,0 +1,38 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNumber, IsPositive } from 'class-validator'; + +export class CreateOrderDto { + @ApiProperty({ + description: 'ID of the asset involved in the order.', + example: 1, + }) + @IsNumber() + @IsPositive() + assetId: number; + + @ApiProperty({ + description: 'Type of the order: BUY or SELL.', + 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 new file mode 100644 index 0000000..ab6c34d --- /dev/null +++ b/src/orders/dto/update-order.dto.ts @@ -0,0 +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() + @IsOptional() + userId?: string; + + @ApiPropertyOptional() + @IsOptional() + assetId?: number; + + @ApiPropertyOptional({ + enum: ['BUY', 'SELL'], + }) + @IsOptional() + @IsEnum(['BUY', 'SELL']) + type?: 'BUY' | 'SELL'; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + @IsPositive() + quantity?: number; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + @IsPositive() + price?: number; + + @ApiPropertyOptional({ + enum: OrderStatusEnum, + }) + @IsOptional() + @IsEnum(OrderStatusEnum) + status?: OrderStatusEnum; +} diff --git a/src/orders/order.controller.ts b/src/orders/order.controller.ts new file mode 100644 index 0000000..bc14a0d --- /dev/null +++ b/src/orders/order.controller.ts @@ -0,0 +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'; +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: 'Order removed successfully' }; + } +} 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..8f00dd5 --- /dev/null +++ b/src/orders/order.service.ts @@ -0,0 +1,121 @@ +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'; + + +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) {} + + async create(userId: string, dto: CreateOrderDto) { + const { assetId, quantity, price, type } = dto; + + + const resolvedUserId = userId || DEFAULT_USER_ID; + + + const user = mockUsers.find(u => u.id === resolvedUserId); + if (!user) throw new NotFoundException('User not found (mock).'); + + + const asset = mockAssets.find(a => a.id === assetId); + if (!asset) throw new NotFoundException('Asset not found (mock).'); + + + 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.'); + + + return this.prisma.order.create({ + data: { + userId: resolvedUserId, + assetId, + quantity, + price, + type, + status: 'PENDING', + }, + }); + } + + 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('Order not found.'); + return order; + } + + async findByUser(userId: string) { + 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: resolvedUserId }, + orderBy: { createdAt: 'desc' }, + }); + } + + async findByAsset(assetId: number) { + 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' }, + }); + } + + async update(id: string, dto: UpdateOrderDto) { + const existing = await this.findOne(id); + + if (existing.status === 'EXECUTED' || existing.status === 'CANCELED') { + throw new BadRequestException('Cannot update executed or canceled order.'); + } + + return this.prisma.order.update({ + where: { id }, + data: dto, + }); + } + + async remove(id: string) { + await this.findOne(id); + return this.prisma.order.delete({ where: { id } }); + } + + getMockAssets() { + return mockAssets; + } + + getMockUsers() { + return mockUsers; + } +}