From 98e9ebc8e90e728c8afdfb48b2cd7d5179506623 Mon Sep 17 00:00:00 2001 From: ismael alves Date: Mon, 8 Jun 2026 15:39:33 -0300 Subject: [PATCH] feat: add router update stock --- src/main.ts | 2 +- src/orders/dto/order.dto.ts | 22 ++++++- src/orders/orders.controller.spec.ts | 87 ++++++++++------------------ src/orders/orders.controller.ts | 11 +++- src/orders/orders.service.ts | 16 +++++ test/app.e2e-spec.ts | 37 +++++++++--- 6 files changed, 105 insertions(+), 70 deletions(-) diff --git a/src/main.ts b/src/main.ts index d19b719..004addf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,7 +14,7 @@ const bootstrap = async () => { .enableVersioning({ type: VersioningType.URI, }); - + const config = new DocumentBuilder() .setTitle('Orders API') .setDescription('this is a simple order management API') diff --git a/src/orders/dto/order.dto.ts b/src/orders/dto/order.dto.ts index 780033b..29b26ac 100644 --- a/src/orders/dto/order.dto.ts +++ b/src/orders/dto/order.dto.ts @@ -1,4 +1,10 @@ -import { IsArray, IsString, ValidateNested, IsNumber } from 'class-validator'; +import { + IsArray, + IsString, + ValidateNested, + IsNumber, + Min, +} from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty, OmitType } from '@nestjs/swagger'; import { OrderStatus } from '../entities/order.entity'; @@ -18,6 +24,7 @@ class OrderItemDto { @IsNumber() @ApiProperty({ description: 'Price' }) + @Min(1) price: number; } @@ -43,4 +50,15 @@ export class OrderDto { export class CreateOrderDto extends OmitType(OrderDto, [ 'id', 'status', -] as const) {} \ No newline at end of file +] as const) {} + +export class UpdateStockDto { + @IsString() + @ApiProperty({ description: 'Product ID' }) + productId: string; + + @IsNumber() + @ApiProperty({ description: 'Stock' }) + @Min(1) + stock: number; +} diff --git a/src/orders/orders.controller.spec.ts b/src/orders/orders.controller.spec.ts index 57400f4..c371e75 100644 --- a/src/orders/orders.controller.spec.ts +++ b/src/orders/orders.controller.spec.ts @@ -1,10 +1,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { OrdersController } from './orders.controller'; import { OrdersService } from './orders.service'; -import { OrderStatus } from './entities/order.entity'; +import { Order, OrderStatus } from './entities/order.entity'; describe('OrdersController', () => { let ordersController: OrdersController; + let order: Order; beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ @@ -13,6 +14,16 @@ describe('OrdersController', () => { }).compile(); ordersController = app.get(OrdersController); + order = ordersController.create({ + items: [ + { + price: 10, + quantity: 1, + productId: '1', + stock: 10, + }, + ], + }); }); it('should create order', () => { @@ -31,58 +42,30 @@ describe('OrdersController', () => { }); it('should find order by ID', () => { - const order = ordersController.create({ - items: [ - { - price: 10, - quantity: 1, - productId: '1', - stock: 10, - }, - ], - }); + const item = order.items[0]; + const productId = item.productId; + const stock = item.stock; + expect( + ordersController.updateStock({ + productId, + stock: stock + 10, + })[0].items[0].stock, + ).toBe(stock + 10); + }); + + it('should find order by ID', () => { expect(ordersController.findOne(order.id)).toBe(order); }); it('should cancel order', () => { - const order = ordersController.create({ - items: [ - { - price: 10, - quantity: 1, - productId: '1', - stock: 10, - }, - ], - }); expect(ordersController.cancel(order.id).status).toBe(OrderStatus.CANCELED); }); it('should generate invoice for order', () => { - const order = ordersController.create({ - items: [ - { - price: 10, - quantity: 1, - productId: '1', - stock: 10, - }, - ], - }); expect(ordersController.invoice(order.id).freeShipping).toBe(false); }); it('should error when generating invoice for Canceled order', () => { - const order = ordersController.create({ - items: [ - { - price: 10, - quantity: 1, - productId: '1', - stock: 10, - }, - ], - }); ordersController.cancel(order.id); expect(() => ordersController.invoice(order.id)).toThrow( 'Canceled order cannot be invoiced', @@ -90,16 +73,6 @@ describe('OrdersController', () => { }); it('should error when generating invoice for already invoiced order', () => { - const order = ordersController.create({ - items: [ - { - price: 10, - quantity: 1, - productId: '1', - stock: 10, - }, - ], - }); ordersController.invoice(order.id); expect(() => ordersController.invoice(order.id)).toThrow( 'Order already invoiced', @@ -107,26 +80,26 @@ describe('OrdersController', () => { }); it('should error when generating invoice for not itens in order', () => { - const order = ordersController.create({ + const empty = ordersController.create({ items: [], }); - expect(() => ordersController.invoice(order.id)).toThrow( + expect(() => ordersController.invoice(empty.id)).toThrow( 'Order must contain items', ); }); it('should error when generating invoice for insufficient stock order', () => { - const order = ordersController.create({ + const insufficient = ordersController.create({ items: [ { price: 10, - quantity: 12, + quantity: 10, productId: '1', - stock: 10, + stock: 1, }, ], }); - expect(() => ordersController.invoice(order.id)).toThrow( + expect(() => ordersController.invoice(insufficient.id)).toThrow( 'Insufficient stock for product 1', ); }); diff --git a/src/orders/orders.controller.ts b/src/orders/orders.controller.ts index 05756e9..af3f5d6 100644 --- a/src/orders/orders.controller.ts +++ b/src/orders/orders.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; import { OrdersService } from './orders.service'; -import { CreateOrderDto, OrderDto } from './dto/order.dto'; +import { CreateOrderDto, OrderDto, UpdateStockDto } from './dto/order.dto'; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; @Controller('orders') @@ -21,6 +21,15 @@ export class OrdersController { return this.ordersService.create(body.items); } + @Patch('stock') + @ApiOperation({ summary: 'Update stock' }) + updateStock( + @Body() + body: UpdateStockDto, + ) { + return this.ordersService.updateStock(body.productId, body.stock); + } + @Get(':id') @ApiOperation({ summary: 'Find order by ID' }) @ApiOkResponse({ type: OrderDto }) diff --git a/src/orders/orders.service.ts b/src/orders/orders.service.ts index 0070365..acb0c49 100644 --- a/src/orders/orders.service.ts +++ b/src/orders/orders.service.ts @@ -10,6 +10,22 @@ import { randomUUID } from 'crypto'; export class OrdersService { private readonly orders: Order[] = []; + updateStock(productId: string, stock: number) { + const ordersToUpdate = this.orders.filter((order) => + order.items.some((item) => item.productId === productId), + ); + + for (const order of ordersToUpdate) { + for (const item of order.items) { + if (item.productId === productId) { + item.stock = stock; + } + } + } + + return ordersToUpdate; + } + create(items: OrderItem[]): Order { const order: Order = { id: randomUUID(), diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index ea85c39..d355a06 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -28,7 +28,7 @@ describe('OrderController (e2e)', () => { }, ], }); - order = response.body; + order = response.body as Order; }); it('/orders (POST)', () => { @@ -45,18 +45,33 @@ describe('OrderController (e2e)', () => { ], }) .expect(201) - .expect((res) => { + .expect((res: { body: Order }) => { expect(res.body.id).toBeDefined(); expect(res.body.status).toBe('PENDING'); expect(res.body.items).toHaveLength(1); }); }); + it('/orders/stock (PATCH)', () => { + return request(app.getHttpServer()) + .patch('/orders/stock') + .send({ + productId: 'product-1', + stock: 20, + }) + .expect(200) + .expect((res: { body: Order[] }) => { + expect(res.body[0].id).toBeDefined(); + expect(res.body[0].status).toBe('PENDING'); + expect(res.body[0].items).toHaveLength(1); + }); + }); + it('/orders (GET)', () => { return request(app.getHttpServer()) .get(`/orders/${order.id}`) .expect(200) - .expect((res) => { + .expect((res: { body: Order }) => { expect(res.body.id).toBeDefined(); expect(res.body.status).toBe('PENDING'); expect(res.body.items).toHaveLength(1); @@ -67,7 +82,7 @@ describe('OrderController (e2e)', () => { return request(app.getHttpServer()) .patch(`/orders/${order.id}/cancel`) .expect(200) - .expect((res) => { + .expect((res: { body: Order }) => { expect(res.body.id).toBeDefined(); expect(res.body.status).toBe('CANCELED'); expect(res.body.items).toHaveLength(1); @@ -78,10 +93,14 @@ describe('OrderController (e2e)', () => { return request(app.getHttpServer()) .patch(`/orders/${order.id}/invoice`) .expect(200) - .expect((res) => { - expect(res.body.orderId).toBeDefined(); - expect(res.body.total).toBeDefined(); - expect(res.body.freeShipping).toBe(false); - }); + .expect( + (res: { + body: { orderId: string; total: number; freeShipping: boolean }; + }) => { + expect(res.body.orderId).toBeDefined(); + expect(res.body.total).toBeDefined(); + expect(res.body.freeShipping).toBe(false); + }, + ); }); });