Skip to content
Merged
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
9 changes: 8 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import express from 'express';
import { StatusCodes } from 'http-status-codes';
import express, { Request, Response, NextFunction } from 'express';
import morgan from 'morgan';
import { routes } from './routes';
import { logger } from './utils/winston';

const app = express();

Expand All @@ -9,4 +11,9 @@ app.use(morgan('tiny'));

app.use('/api/v1', routes);

app.use((error: Error, _req: Request, res: Response, _next: NextFunction) => {
logger.error(error);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: error.message });
})

export default app;
4 changes: 2 additions & 2 deletions src/business/health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export function healthcheck() {

return {
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now()
message: "OK",
timestamp: Date.now(),
};
}
25 changes: 25 additions & 0 deletions src/business/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { product } from "./../models/product";
import { IProduct } from "../interfaces/product";
import { productListParser, productParser } from "../parsers/product";

export async function createBusiness(data: IProduct): Promise<number> {
if (product.checkExists(data.sku)) {
throw new Error("Product already exists");
}

return product.create(data);
}

export async function findAll(): Promise<IProduct[]> {
const productList = product.findAll();
return productListParser(productList);
}

export async function findBySku(sku: number): Promise<IProduct> {
const response = product.findBySku(sku);
if (response.length === 0) {
throw new Error("Product not found");
}

return productParser(response[0]);
}
18 changes: 10 additions & 8 deletions src/controllers/health.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Request, Response } from 'express';
import { StatusCodes } from 'http-status-codes';
import { healthcheck } from '../business/health';
import { NextFunction } from "express";
import { Request, Response } from "express";
import { StatusCodes } from "http-status-codes";
import { healthcheck } from "../business/health";

async function health(_req: Request, res: Response): Promise<Response> {
export async function health(
_req: Request,
res: Response,
next: NextFunction
): Promise<Response> {
try {
const response = healthcheck();
return res.status(StatusCodes.OK).json(response);
} catch (error) {
console.log(error);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({message: error.message });
next(error);
}
}

export default health;
52 changes: 52 additions & 0 deletions src/controllers/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Request, Response, NextFunction } from "express";
import { StatusCodes } from "http-status-codes";
import { logger } from "../utils/winston";
import { createBusiness, findAll, findBySku } from "../business/product";

export async function createController(
req: Request,
res: Response,
next: NextFunction
): Promise<Response> {
try {
const response = await createBusiness(req.body);
return res.status(StatusCodes.CREATED).json(response);
} catch (error) {
logger.error(error);

if (error.message === "Product already exists") {
return res.status(StatusCodes.CONFLICT).json({ message: error.message });
}
next(error);
}
}

export async function findAllController(
_req: Request,
res: Response,
next: NextFunction
): Promise<Response> {
try {
const response = await findAll();
return res.status(StatusCodes.OK).json(response);
} catch (error) {
next(error);
}
}

export async function findBySkuController(
req: Request,
res: Response,
next: NextFunction
): Promise<Response> {
try {
const response = await findBySku(parseInt(req.params.sku));
return res.status(StatusCodes.OK).json(response);
} catch (error) {
if (error.message === "Product not found") {
return res.status(StatusCodes.NOT_FOUND).json({ message: error.message });
}

next(error);
}
}
15 changes: 15 additions & 0 deletions src/interfaces/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
interface IWarehouse {
locality: string;
quantity: number;
type: string;
}

export interface IProduct {
sku: number;
name: string;
inventory: {
quantity?: number;
warehouses: IWarehouse[];
};
isMarketable?: boolean;
}
45 changes: 45 additions & 0 deletions src/models/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { IProduct } from "../interfaces/product";

class ProductModel {
private productsDatabase: IProduct[] = [];

create(data: IProduct): number | null {
this.productsDatabase.push(data);
return data.sku;
}

findAll(): Array<IProduct> {
return this.productsDatabase;
}

findBySku(sku: number): IProduct[] {
return this.productsDatabase.filter((item) => {
return item.sku === sku;
});
}

update(productIndex: number, data: IProduct): IProduct {
this.productsDatabase[productIndex] = data;
return data;
}

remove(sku: number): void {
this.productsDatabase = this.productsDatabase.filter(function (obj) {
return obj.sku !== sku;
});
}

checkExists(sku: number): number | null {
if (this.productsDatabase.some((el) => el.sku === sku)) {
return sku;
}

return null;
}

getProductIndex(sku: number): number {
return this.productsDatabase.findIndex((item) => item.sku === sku);
}
}

export const product = new ProductModel();
32 changes: 32 additions & 0 deletions src/parsers/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { IProduct } from "../interfaces/product";

export function productParser(product: IProduct): IProduct {
product.inventory.quantity = product.inventory.warehouses.reduce(
(acc, current) => acc + current.quantity,
0
);
product.isMarketable = false;

if (product.inventory.quantity > 0) {
product.isMarketable = true;
}

return {
sku: product.sku,
name: product.name,
inventory: {
quantity: product.inventory.quantity,
warehouses: product.inventory.warehouses
},
isMarketable: product.isMarketable
};
}

export function productListParser(productList: IProduct[]): IProduct[] {
const parsedProductList: IProduct[] = [];
productList.forEach((product) => {
parsedProductList.push(productParser(product));
});

return parsedProductList;
}
2 changes: 1 addition & 1 deletion src/routes/health.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Router } from "express";
import health from "../controllers/health";
import { health } from "../controllers/health";

const routes = Router();

Expand Down
6 changes: 4 additions & 2 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Router } from "express";
import healthRoutes from './health';
import healthRoutes from "./health";
import productRoutes from "./product";

const routes: Router = Router();

routes.use('/health', healthRoutes);
routes.use("/health", healthRoutes);
routes.use("/products", productRoutes);

export { routes };
14 changes: 14 additions & 0 deletions src/routes/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Router } from "express";
import {
createController,
findAllController,
findBySkuController,
} from "../controllers/product";

const routes = Router();

routes.post("/", createController);
routes.get("/", findAllController);
routes.get("/:sku", findBySkuController);

export default routes;
128 changes: 128 additions & 0 deletions tests/integration/products/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import request from "supertest";
import app from "../../../src/app";
import * as mocks from "./mocks/products-mock";

describe("/api/v1/product endpoint", () => {
test("[POST] should return 201 when product was created with success", async () => {
const res = await request(app)
.post("/api/v1/products")
.send(mocks.productOne);
expect(res.statusCode).toEqual(201);

expect(res.body).toBeTruthy();
expect(res.body).toBe(43264);
});

test("[POST] should return 201 when another product was created with success", async () => {
const res = await request(app)
.post("/api/v1/products")
.send(mocks.productTwo);
expect(res.statusCode).toEqual(201);

expect(res.body).toBeTruthy();
expect(res.body).toBe(43562);
});

test("[POST] should return 201 when another product was created with success", async () => {
const res = await request(app)
.post("/api/v1/products")
.send(mocks.productThree);
expect(res.statusCode).toEqual(201);

expect(res.body).toBeTruthy();
expect(res.body).toBe(44985);
});

test("[POST] should return 201 when another product was created with success", async () => {
const res = await request(app)
.post("/api/v1/products")
.send(mocks.productFour);
expect(res.statusCode).toEqual(201);

expect(res.body).toBeTruthy();
expect(res.body).toBe(44989);
});

test("[POST] should return 409 when product was created already exists", async () => {
const res = await request(app)
.post("/api/v1/products")
.send(mocks.productOne);
expect(res.statusCode).toEqual(409);

expect(res.body).toBeTruthy();
expect(res.body).toHaveProperty("message", "Product already exists");
});

test("[GET] should return 200 when products exists", async () => {
const res = await request(app).get("/api/v1/products");
expect(res.statusCode).toEqual(200);

expect(res.body).toBeTruthy();
expect(res.body).toHaveLength(4);
expect(res.body[0]).toHaveProperty("sku", 43264);
expect(res.body[0]).toHaveProperty(
"name",
"L'Oréal Professionnel Expert Absolut Repair Cortex Lipidium - Máscara de Reconstrução 500g"
);
expect(res.body[0]).toHaveProperty("inventory");
expect(res.body[0].inventory).toHaveProperty("quantity", 15);
expect(res.body[0].inventory).toHaveProperty("warehouses");
expect(res.body[0].inventory.warehouses[0]).toHaveProperty(
"locality",
"SP"
);
expect(res.body[0].inventory.warehouses[0]).toHaveProperty("quantity", 12);
expect(res.body[0].inventory.warehouses[0]).toHaveProperty(
"type",
"ECOMMERCE"
);
expect(res.body[0].inventory.warehouses[1]).toHaveProperty(
"locality",
"MOEMA"
);
expect(res.body[0].inventory.warehouses[1]).toHaveProperty("quantity", 3);
expect(res.body[0].inventory.warehouses[1]).toHaveProperty(
"type",
"PHYSICAL_STORE"
);
expect(res.body[0]).toHaveProperty("isMarketable", true);
});

test("[GET] should return 200 when get product by id and isMarketable is false", async () => {
const res = await request(app).get("/api/v1/products/44989");
expect(res.statusCode).toEqual(200);

expect(res.body).toBeTruthy();
expect(res.body).toHaveProperty("sku", 44989);
expect(res.body).toHaveProperty("name", "Produto Teste");
expect(res.body).toHaveProperty("inventory");
expect(res.body.inventory).toHaveProperty("quantity", 0);
expect(res.body.inventory).toHaveProperty("warehouses");
expect(res.body.inventory.warehouses[0]).toHaveProperty("locality", "SP");
expect(res.body.inventory.warehouses[0]).toHaveProperty("quantity", 0);
expect(res.body.inventory.warehouses[0]).toHaveProperty(
"type",
"ECOMMERCE"
);

expect(res.body.inventory.warehouses[1]).toHaveProperty(
"locality",
"MOEMA"
);
expect(res.body.inventory.warehouses[1]).toHaveProperty("quantity", 0);
expect(res.body.inventory.warehouses[1]).toHaveProperty(
"type",
"PHYSICAL_STORE"
);
expect(res.body.inventory.warehouses[2]).toHaveProperty(
"locality",
"FRANCA"
);
expect(res.body.inventory.warehouses[2]).toHaveProperty("quantity", 0);
expect(res.body.inventory.warehouses[2]).toHaveProperty(
"type",
"PHYSICAL_STORE"
);
expect(res.body).toHaveProperty("isMarketable", false);
});
});
Loading