Skip to content

Commit

Permalink
EF-81-83 Customer can view and add to cart
Browse files Browse the repository at this point in the history
  • Loading branch information
MinhhTien authored and hideonbush106 committed Jan 25, 2024
1 parent 701dabd commit 87d7b4e
Show file tree
Hide file tree
Showing 18 changed files with 330 additions and 24 deletions.
8 changes: 6 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { join } from 'path'
import configuration from '@src/config'
import { CommonModule } from '@common/common.module'
import { CustomerModule } from '@customer/customer.module'
import { AuthModule } from './auth/auth.module'
import { AuthModule } from '@auth/auth.module'
import { ProductModule } from '@product/product.module'
import { CartModule } from '@cart/cart.module'

@Module({
imports: [
Expand Down Expand Up @@ -53,7 +55,9 @@ import { AuthModule } from './auth/auth.module'
]),
CommonModule,
CustomerModule,
AuthModule
AuthModule,
ProductModule,
CartModule
],
controllers: [AppController],
providers: [AppService]
Expand Down
6 changes: 3 additions & 3 deletions src/auth/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ApiBadRequestResponse, ApiBearerAuth, ApiBody, ApiOkResponse, ApiTags }
import { ErrorResponse } from '@common/contracts/dto'
import { LoginReqDto } from '@auth/dto/login.dto'
import { AuthService } from '@auth/services/auth.service'
import { TokenResDto } from '@auth/dto/token.dto'
import { ResponseTokenDto, TokenResDto } from '@auth/dto/token.dto'
import { UserSide } from '@common/contracts/constant'
import { JwtAuthGuard } from '@auth/guards/jwt-auth.guard'

Expand All @@ -14,7 +14,7 @@ export class AuthController {

@Post('customer/login')
@ApiBody({ type: LoginReqDto })
@ApiOkResponse({ type: TokenResDto })
@ApiOkResponse({ type: ResponseTokenDto })
@ApiBadRequestResponse({ type: ErrorResponse })
async login(@Body() loginReqDto: LoginReqDto): Promise<TokenResDto> {
const res = await this.authService.login(loginReqDto, UserSide.CUSTOMER)
Expand All @@ -25,7 +25,7 @@ export class AuthController {
@UseGuards(JwtAuthGuard.REFRESH_TOKEN)
@Post('customer/refresh')
@ApiBearerAuth()
@ApiOkResponse({ type: TokenResDto })
@ApiOkResponse({ type: ResponseTokenDto })
@ApiBadRequestResponse({ type: ErrorResponse })
async refreshToken(@Req() req): Promise<TokenResDto> {
const res = await this.authService.refreshAccessToken(req.user.id, UserSide.CUSTOMER)
Expand Down
5 changes: 5 additions & 0 deletions src/auth/dto/token.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export class TokenResDto {
refreshToken: string
}

export class ResponseTokenDto {
@ApiProperty()
data: TokenResDto
}

export class RefreshTokenReqDto {
@ApiProperty()
@IsNotEmpty()
Expand Down
2 changes: 1 addition & 1 deletion src/auth/strategies/jwt-access.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class JwtAccessStrategy extends PassportStrategy(Strategy, 'jwt-access')
}

async validate(payload: AccessTokenPayload) {
return { id: payload.sub, name: payload.name, role: payload.role }
return { _id: payload.sub, name: payload.name, role: payload.role }
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/auth/strategies/jwt-refresh.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh'
}

async validate(payload: RefreshTokenPayload) {
return { id: payload.sub, role: payload.role }
return { _id: payload.sub, role: payload.role }
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/cart/cart.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common'
import { MongooseModule } from '@nestjs/mongoose'

import { Cart, CartSchema } from '@cart/schemas/cart.schema'
import { CartController } from '@cart/controllers/cart.controller'
import { CartService } from '@cart/services/cart.service'
import { CartRepository } from '@cart/repositories/cart.repository'

@Module({
imports: [MongooseModule.forFeature([{ name: Cart.name, schema: CartSchema }])],
controllers: [CartController],
providers: [CartService, CartRepository],
exports: [CartService]
})
export class CartModule {}
36 changes: 36 additions & 0 deletions src/cart/controllers/cart.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Body, Controller, Get, Param, Patch, Post, Req, UseGuards } from '@nestjs/common'
import { ApiBadRequestResponse, ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger'
import * as _ from 'lodash'

import { ErrorResponse, ResponseSuccessDto } from '@common/contracts/dto'
import { CartService } from '@cart/services/cart.service'
import { JwtAuthGuard } from '@auth/guards/jwt-auth.guard'
import { RolesGuard } from '@auth/guards/roles.guard'
import { UserRole } from '@common/contracts/constant'
import { Roles } from '@auth/decorators/roles.decorator'
import { AddToCartDto, ResponseCartDto } from '@cart/dto/cart.dto'

@ApiTags('Cart')
@ApiBearerAuth()
@Roles(UserRole.CUSTOMER)
@UseGuards(JwtAuthGuard.ACCESS_TOKEN, RolesGuard)
@Controller('cart')
export class CartController {
constructor(private readonly cartService: CartService) {}

@Post()
@ApiOkResponse({ type: ResponseSuccessDto })
@ApiBadRequestResponse({ type: ErrorResponse })
async addToCart(@Req() req, @Body() addToCartDto: AddToCartDto) {
addToCartDto.customerId = _.get(req, 'user._id')
const customer = await this.cartService.addToCart(addToCartDto)
return customer
}

@Get()
@ApiOkResponse({ type: ResponseCartDto })
async getListCard(@Req() req) {
const customerId = _.get(req, 'user._id')
return await this.cartService.getListCard(customerId)
}
}
11 changes: 11 additions & 0 deletions src/cart/dto/cart.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Cart, ItemDto } from '@cart/schemas/cart.schema'
import { ApiProperty } from '@nestjs/swagger'

export class AddToCartDto extends ItemDto {
customerId?: string
}

export class ResponseCartDto {
@ApiProperty()
data: Cart
}
13 changes: 13 additions & 0 deletions src/cart/repositories/cart.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PaginateModel } from 'mongoose'
import { Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'

import { AbstractRepository } from '@common/repositories'
import { Cart, CartDocument } from '@cart/schemas/cart.schema'

@Injectable()
export class CartRepository extends AbstractRepository<CartDocument> {
constructor(@InjectModel(Cart.name) model: PaginateModel<CartDocument>) {
super(model)
}
}
67 changes: 67 additions & 0 deletions src/cart/schemas/cart.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { HydratedDocument } from 'mongoose'
import * as paginate from 'mongoose-paginate-v2'
import { ApiProperty } from '@nestjs/swagger'
import { Transform, Type } from 'class-transformer'
import { Status } from '@common/contracts/constant'
import { IsNotEmpty, Min, ValidateNested } from 'class-validator'

import { Product } from '@product/schemas/product.schema'

export class ItemDto {
@ApiProperty()
@IsNotEmpty()
@ValidateNested()
product: Product

@ApiProperty()
@IsNotEmpty()
@Min(1)
quantity: number
}

export type CartDocument = HydratedDocument<Cart>

@Schema({
collection: 'carts',
timestamps: {
createdAt: true,
updatedAt: true
},
toJSON: {
transform(doc, ret) {
delete ret.__v
}
}
})
export class Cart {
constructor(id?: string) {
this._id = id
}

@ApiProperty()
@Transform(({ value }) => value?.toString())
_id: string

@Prop({ type: String, required: true })
customerId: string;

@ApiProperty({ isArray: true, type: ItemDto })
@ValidateNested()
@Prop({ type: Array<ItemDto>, required: true })
items: ItemDto[]

@ApiProperty()
@Prop({ type: Number, required: true })
totalAmount: number

@Prop({
enum: Status,
default: Status.ACTIVE
})
status: Status
}

export const CartSchema = SchemaFactory.createForClass(Cart)

CartSchema.plugin(paginate)
71 changes: 71 additions & 0 deletions src/cart/services/cart.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { BadRequestException, Injectable } from '@nestjs/common'
import { Errors } from '@src/common/contracts/error'
import { CartRepository } from '@cart/repositories/cart.repository'
import { AddToCartDto } from '@cart/dto/cart.dto'

@Injectable()
export class CartService {
constructor(private readonly cartRepository: CartRepository) {}

public async addToCart(addToCartDto: AddToCartDto) {
const { customerId, product, quantity } = addToCartDto
const cart = await this.cartRepository.findOne({
conditions: {
customerId
}
})
const amount = product.price * quantity
if (!cart) {
// create new cart
await this.cartRepository.create({
customerId,
items: [{ product, quantity }],
totalAmount: amount
})
} else {
const { _id, items } = cart
// update cart
// check existed item
const existedItemIndex = items.findIndex((item) => item.product._id === product._id)

if (existedItemIndex === -1) {
// push new item in the first element
items.unshift({ product, quantity })
} else {
// update quantity in existed item
items[existedItemIndex].quantity += quantity
}
const totalAmount = (cart.totalAmount += amount)
await this.cartRepository.findOneAndUpdate(
{
_id
},
{
items,
totalAmount
}
)
}
return { success: true }
}

public async getListCard(customerId: string) {
const cartList = await this.cartRepository.findOne({
conditions: { customerId },
projection: {
_id: 1,
items: 1,
totalAmount: 1
}
})
if (!cartList) {
const newCartList = await this.cartRepository.create({
customerId,
items: [],
totalAmount: 0
})
return { _id: newCartList._id, items: [], totalAmount: 0 }
}
return cartList
}
}
19 changes: 18 additions & 1 deletion src/common/contracts/dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
import { ApiProperty } from '@nestjs/swagger'
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { IsPositive } from 'class-validator';

export class PaginateDto {
@ApiProperty({ type: Number })
@ApiPropertyOptional()
@IsPositive()
page = 1;

@ApiProperty({ type: Number })
@ApiPropertyOptional()
@IsPositive()
limit = 10;

@ApiProperty({ type: String, example: 'field1.asc_field2.desc' })
@ApiPropertyOptional()
sort: Record<string, 1 | -1>;
}

export class BooleanResponseDto {
@ApiProperty()
Expand Down
56 changes: 56 additions & 0 deletions src/common/decorators/pagination.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ExecutionContext, createParamDecorator } from '@nestjs/common'
import * as _ from 'lodash'

export interface PaginationParams {
page: number
limit: number
sort: Record<string, any>
}

const DEFAULT_LIMIT = 10

export const handlePagination: (request: any) => PaginationParams = (request) => {
const paginationParams = {
page: request.query.page,
limit: request.query.limit,
sort: request.query.sort
}
paginationParams.page = Number(paginationParams.page) > 0 ? Number(paginationParams.page) : 1
paginationParams.limit = Number(paginationParams.limit) > 0 ? Number(paginationParams.limit) : DEFAULT_LIMIT
if (_.isEmpty(paginationParams.sort)) {
paginationParams.sort = {
createdAt: -1
}
} else {
const result = {}
const sortFields = paginationParams.sort.split('_')
sortFields.forEach((item) => {
const sortType = item.indexOf('.asc') !== -1 ? '.asc' : '.desc'
result[item.replace(sortType, '')] = sortType === '.asc' ? 1 : -1
})
paginationParams.sort = result
}
return paginationParams
}

/**
* How to use
- In controller:
@ApiQuery({ type: PaginationQuery })
@Get()
async list(
@Pagination() paginationParams: PaginationParams,
@Query() filterDto: FilterDto,
) {
return await this.service.list(filterDto, paginationParams);
}
- In service:
async list(filterDto, paginationParams) {
return await this.service.list(filterDto, paginationParams);
}
*/
export const Pagination = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return handlePagination(request)
})
Loading

0 comments on commit 87d7b4e

Please sign in to comment.