Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
22 changes: 20 additions & 2 deletions src/orders/dto/order.dto.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -18,6 +24,7 @@ class OrderItemDto {

@IsNumber()
@ApiProperty({ description: 'Price' })
@Min(1)
price: number;
}

Expand All @@ -43,4 +50,15 @@ export class OrderDto {
export class CreateOrderDto extends OmitType(OrderDto, [
'id',
'status',
] as const) {}
] as const) {}

export class UpdateStockDto {
@IsString()
@ApiProperty({ description: 'Product ID' })
productId: string;

@IsNumber()
@ApiProperty({ description: 'Stock' })
@Min(1)
stock: number;
}
87 changes: 30 additions & 57 deletions src/orders/orders.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -13,6 +14,16 @@ describe('OrdersController', () => {
}).compile();

ordersController = app.get<OrdersController>(OrdersController);
order = ordersController.create({
items: [
{
price: 10,
quantity: 1,
productId: '1',
stock: 10,
},
],
});
});

it('should create order', () => {
Expand All @@ -31,102 +42,64 @@ 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',
);
});

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',
);
});

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',
);
});
Expand Down
11 changes: 10 additions & 1 deletion src/orders/orders.controller.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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 })
Expand Down
16 changes: 16 additions & 0 deletions src/orders/orders.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
37 changes: 28 additions & 9 deletions test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('OrderController (e2e)', () => {
},
],
});
order = response.body;
order = response.body as Order;
});

it('/orders (POST)', () => {
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
},
);
});
});
Loading