Skip to content

Commit

Permalink
Merge pull request #2 from educata/feat/products-cart-2
Browse files Browse the repository at this point in the history
feat/products-cart-2 & added cart endpoints
  • Loading branch information
KostaD02 authored Aug 21, 2023
2 parents f0ad4f4 + 57e33bc commit 8281ab0
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 31 deletions.
4 changes: 4 additions & 0 deletions src/enums/exceptions.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export enum ExceptionStatusKeys {

export enum ProductExceptionKeys {
ProductNotFound = 'errors.product_not_found',
ProductStockOutnumbered = 'errors.product_stock_outnumbered',
ProductStockSoldBeforeCheckout = 'errors.product_stock_sold_before_checkout',
RatingNotNumber = 'errors.rating_not_number',
RatingTooLow = 'errors.rating_too_low',
RatingTooHigh = 'errors.rating_too_high',
Expand Down Expand Up @@ -47,4 +49,6 @@ export enum AuthExpectionKeys {
export enum CartExpectionKeys {
UserDontHaveCart = 'errors.user_cart_not_exists',
UserCartAlreadyExists = 'errors.user_cart_already_exists',
CartDontHaveThisItem = 'errors.cart_do_not_have_this_item',
CartAlreadyDeleted = 'errors.cart_already_deleted',
}
14 changes: 7 additions & 7 deletions src/modules/shop/cart/carts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { CartsService } from './carts.service';
import { CurrentUser, CurrentUserInterceptor, JwtGuard } from 'src/shared';
import { UserPayload } from 'src/interfaces';
import { CartDto } from '../dtos';
import { CartDto, ProductIdDto } from '../dtos';
@Controller('shop/cart')
@UseGuards(JwtGuard)
@UseInterceptors(CurrentUserInterceptor)
Expand All @@ -29,8 +29,8 @@ export class CartsController {
}

@Post('checkout')
checkout() {
return {};
checkout(@CurrentUser() user: UserPayload) {
return this.cartsService.checkout(user);
}

@Patch('product')
Expand All @@ -39,12 +39,12 @@ export class CartsController {
}

@Delete('product')
deleteCartItem() {
return {};
deleteCartItem(@CurrentUser() user: UserPayload, @Body() body: ProductIdDto) {
return this.cartsService.deleteCartItem(user, body);
}

@Delete()
clearCart() {
return {};
clearCart(@CurrentUser() user: UserPayload) {
return this.cartsService.clearCart(user);
}
}
170 changes: 154 additions & 16 deletions src/modules/shop/cart/carts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ExceptionStatusKeys,
ProductExceptionKeys,
} from 'src/enums';
import { UserPayload } from 'src/interfaces';
import { UserPayload, CartProduct } from 'src/interfaces';
import {
Cart,
CartDocument,
Expand All @@ -16,7 +16,7 @@ import {
UserDocument,
} from 'src/schemas';
import { ExceptionService } from 'src/shared';
import { CartDto } from '../dtos';
import { CartDto, ProductIdDto } from '../dtos';

@Injectable()
export class CartsService {
Expand Down Expand Up @@ -63,20 +63,28 @@ export class CartsService {
);
}

if (product.stock < body.quantity) {
this.exceptionService.throwError(
ExceptionStatusKeys.BadRequest,
`Product stock is outnumbered, product have only ${product.stock} item in stock`,
ProductExceptionKeys.ProductStockOutnumbered,
);
}

const cart = await this.cartModel.create({
userId: userPayload._id,
createdAt: new Date().toISOString(),
total: {
price: {
current: product.price.current * body.quanity,
beforeDiscount: product.price.beforeDiscount * body.quanity,
current: product.price.current * body.quantity,
beforeDiscount: product.price.beforeDiscount * body.quantity,
},
quantity: body.quanity,
quantity: body.quantity,
products: 1,
},
products: [
{
quantity: body.quanity,
quantity: body.quantity,
pricePerQuantity: product.price.current,
beforeDiscountPrice: product.price.beforeDiscount,
productId: body.id,
Expand Down Expand Up @@ -111,42 +119,172 @@ export class CartsService {
);
}

if (product.stock < body.quantity) {
this.exceptionService.throwError(
ExceptionStatusKeys.BadRequest,
`Product stock is outnumbered, product have only ${product.stock} item in stock`,
ProductExceptionKeys.ProductStockOutnumbered,
);
}

const cart = await this.cartModel.findOne({ _id: user.cartID });
const itemIndex = cart.products.findIndex(
(product) => product.productId === body.id,
);

if (itemIndex === -1) {
cart.products.push({
quantity: body.quanity,
quantity: body.quantity,
pricePerQuantity: product.price.current,
productId: product.id,
beforeDiscountPrice: product.price.beforeDiscount,
});
} else {
if (body.quanity <= 0) {
if (body.quantity <= 0) {
cart.products.splice(itemIndex, 1);
} else {
cart.products[itemIndex].quantity = body.quanity;
cart.products[itemIndex].quantity = body.quantity;
}
}

cart.total = {
cart.total = this.calculateCartTotal(cart.products);

await cart.save();
return cart;
}

async deleteCartItem(userPayload: UserPayload, body: ProductIdDto) {
const user = await this.userModel.findOne({ _id: userPayload._id });

if (user && !user.cartID) {
this.exceptionService.throwError(
ExceptionStatusKeys.Conflict,
'User has to create cart first',
CartExpectionKeys.UserDontHaveCart,
);
}

const product = await this.productModel.findOne({ _id: body.id });

if (!product) {
this.exceptionService.throwError(
ExceptionStatusKeys.NotFound,
`Product with ${body.id} id not found`,
ProductExceptionKeys.ProductNotFound,
);
}

const cart = await this.cartModel.findOne({ _id: user.cartID });

const productIndex = cart.products.findIndex(
(product) => product.productId === body.id,
);

if (productIndex === -1) {
this.exceptionService.throwError(
ExceptionStatusKeys.NotFound,
`Cart doesn't have item with this ${body.id} id`,
CartExpectionKeys.CartDontHaveThisItem,
);
}

cart.products.splice(productIndex, 1);
cart.total = this.calculateCartTotal(cart.products);

await cart.save();
return cart;
}

private calculateCartTotal(products: CartProduct[]) {
return {
price: {
current: cart.products.reduce((prev, curr) => {
current: products.reduce((prev, curr) => {
return prev + curr.pricePerQuantity * curr.quantity;
}, 0),
beforeDiscount: cart.products.reduce((prev, curr) => {
beforeDiscount: products.reduce((prev, curr) => {
return prev + curr.beforeDiscountPrice * curr.quantity;
}, 0),
},
quantity: cart.products.reduce((prev, curr) => {
quantity: products.reduce((prev, curr) => {
return prev + curr.quantity;
}, 0),
products: cart.products.length,
products: products.length,
};
}

await cart.save();
return cart;
async clearCart(userPayload: UserPayload) {
const user = await this.userModel.findOne({ _id: userPayload._id });

if (user && !user.cartID) {
this.exceptionService.throwError(
ExceptionStatusKeys.Conflict,
'User has to create cart first',
CartExpectionKeys.UserDontHaveCart,
);
}

const cart = await this.cartModel.findOneAndDelete({ _id: user.cartID });

if (!cart) {
this.exceptionService.throwError(
ExceptionStatusKeys.BadRequest,
'User cart already was deleted',
CartExpectionKeys.CartAlreadyDeleted,
);
}

user.cartID = '';
await user.save();

return { success: true };
}

async checkout(userPayload: UserPayload) {
// TODO: do we need to verify payment ? like adding card and it's validation
const user = await this.userModel.findOne({ _id: userPayload._id });

if (user && !user.cartID) {
this.exceptionService.throwError(
ExceptionStatusKeys.NotFound,
"User doesn't have cart",
CartExpectionKeys.UserDontHaveCart,
);
}

const cart = await this.cartModel.findOne({ _id: user.cartID });
const cacheOfProducts = [];
let total = 0;
cart.products.forEach(async (doc, index) => {
const product = await this.productModel.findOne({ _id: doc.productId });
if (product) {
const productStock = product.stock;
product.stock -= doc.quantity;
if (product.stock < 0) {
this.exceptionService.throwError(
ExceptionStatusKeys.Conflict,
`Product with this ${doc.productId} id, already sold some items, currently can't checkout becouse product have ${productStock} and user want's ${doc.quantity}`,
ProductExceptionKeys.ProductStockSoldBeforeCheckout,
);
return;
}
total += doc.quantity;
cacheOfProducts.push(product);
}
if (index + 1 === cart.products.length) {
cacheOfProducts.forEach(async (item) => {
await this.productModel.findOneAndUpdate(
{ _id: item.id },
{ stock: item.stock },
);
});
}
});
user.cartID = '';
await user.save();
await cart.deleteOne();
return {
success: true,
message: `Stocks were updated, currently ${total} item were sold. Cart will be cleared, user have to create new cart with POST request`,
};
}
}
2 changes: 1 addition & 1 deletion src/modules/shop/dtos/cart.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// TODO: add validation
export class CartDto {
id: string; // TODO: validation for mongooseId
quanity: number;
quantity: number;
}
15 changes: 8 additions & 7 deletions src/modules/shop/dtos/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export * from './create-product.dto';
export * from './product.dto';
export * from './update-product.dto';
export * from './search-product-query.dto';
export * from './pagination-product.dto';
export * from './update-rating-product.dto';
export * from './cart.dto';
export * from './create-product.dto';
export * from './product.dto';
export * from './update-product.dto';
export * from './search-product-query.dto';
export * from './pagination-product.dto';
export * from './update-rating-product.dto';
export * from './cart.dto';
export * from './product-id.dto';
4 changes: 4 additions & 0 deletions src/modules/shop/dtos/product-id.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// TODO: add validation
export class ProductIdDto {
id: string;
}

0 comments on commit 8281ab0

Please sign in to comment.