Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: unauthorized error in product update, product stock data retrieval and comment addition #1331

Merged
Merged
2 changes: 0 additions & 2 deletions src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,10 @@ export default class AuthenticationService {
identifierType: 'email',
});

console.log('userExists', userExists);
if (userExists) {
throw new CustomHttpException(SYS_MSG.USER_ACCOUNT_EXIST, HttpStatus.BAD_REQUEST);
}

console.log('createUserDto', createUserDto);
const user = await this.userService.createUser(createUserDto, manager);

if (!user) {
Expand Down
18 changes: 0 additions & 18 deletions src/modules/comments/entities/comment.entity.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/modules/comments/entities/comments.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export class Comment extends AbstractBaseEntity {
@ManyToOne(() => User, user => user.comments, { cascade: true })
user: User;

@Column({ nullable: false })
@Column({ nullable: true })
model_id: string;

@Column()
@Column({ nullable: true })
model_type: string;
}
4 changes: 2 additions & 2 deletions src/modules/email/email.consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default class EmailQueueConsumer {
}
}

@Process('in-app-notification')
@Process('login-otp')
async sendLoginOtpEmailJob(job: Job<MailInterface>) {
try {
const {
Expand All @@ -113,7 +113,7 @@ export default class EmailQueueConsumer {
}
}

@Process('login-otp')
@Process('in-app-notification')
async sendNotificationMail(job: Job<MailInterface>) {
try {
const {
Expand Down
71 changes: 61 additions & 10 deletions src/modules/email/hng-templates/login-otp.hbs
Original file line number Diff line number Diff line change
@@ -1,20 +1,71 @@
<html lang='en'><head><meta charset='UTF-8' /><meta
name='viewport'
content='width=device-width, initial-scale=1.0'
/><title>Email Confirmation</title><style>body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin:
0; padding: 0; } .container { width: 100%; max-width: 600px; margin: 0 auto; background-color: #ffffff; padding:
20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } .header { text-align: center; padding-bottom:
20px; } .header img { width: 120px; } .content { text-align: center; padding: 20px; } .content h1 { color: #333;
font-size: 24px; margin-bottom: 15px; } .content p { color: #666; font-size: 16px; line-height: 1.5;
margin-bottom: 20px; } .btn { display: inline-block; padding: 12px 25px; color: #ffffff; background-color:
#28a745; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 16px; margin-top: 20px; }
.footer { text-align: center; padding-top: 20px; font-size: 12px; color: #888888; } .footer a { color: #28a745;
text-decoration: none; }</style></head><body><div class='container'><div class='header'><img
/><title>Email Confirmation</title><style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
padding-bottom: 20px;
}
.header img {
width: 120px;
}
.content {
text-align: center;
padding: 20px;
}
.content h1 {
color: #333;
font-size: 24px;
margin-bottom: 15px;
}
.content p {
color: #666;
font-size: 16px;
line-height: 1.5;
margin-bottom: 20px;
}
.btn {
display: inline-block;
padding: 12px 25px;
color: #ffffff;
background-color: #28a745;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
font-size: 16px;
margin-top: 20px;
}
.footer {
text-align: center;
padding-top: 20px;
font-size: 12px;
color: #888888;
}
.footer a {
color: #28a745;
text-decoration: none;
}
</style></head><body><div class='container'><div class='header'><img
src='https://www.shutterstock.com/image-vector/circle-line-simple-design-logo-600nw-2174926871.jpg'
alt='Company Logo'
/></div><div class='content'><h1>Confirm Your Email Address</h1><p>Hello {{email}}</p><p>Thank you for
registering. Please confirm your email address by entering the otp:
<strong>{{otp}}</strong></p><p>If you didn't create an account with us, you can safely ignore this email.</p></div><div
<strong>{{token}}</strong></p><p>If you didn't create an account with us, you can safely ignore this email.</p></div><div
class='footer'
><p>&copy; 2024 Company Name. All rights reserved.</p><p><a
href='https://example.com/unsubscribe'
Expand Down
8 changes: 4 additions & 4 deletions src/modules/products/entities/product.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AbstractBaseEntity } from '../../../entities/base.entity';
import { AbstractBaseEntity } from '@entities/base.entity';
import { Column, DeleteDateColumn, Entity, ManyToOne, OneToMany } from 'typeorm';
import { Comment } from '../../../modules/comments/entities/comments.entity';
import { Organisation } from '../../../modules/organisations/entities/organisations.entity';
import { Comment } from '@modules/comments/entities/comments.entity';
import { Organisation } from '@modules/organisations/entities/organisations.entity';
import { Cart } from '../../dashboard/entities/cart.entity';
import { OrderItem } from '../../dashboard/entities/order-items.entity';
import { Review } from './review.entity';
Expand Down Expand Up @@ -64,7 +64,7 @@ export class Product extends AbstractBaseEntity {
@DeleteDateColumn()
deletedAt?: Date;

@OneToMany(() => Comment, comment => comment.product)
@OneToMany(() => Comment, comment => comment.product, { onUpdate: 'CASCADE' })
comments?: Comment[];

@OneToMany(() => OrderItem, orderItem => orderItem.product)
Expand Down
11 changes: 5 additions & 6 deletions src/modules/products/products.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { AuthGuard } from '../../guards/auth.guard';
import { CurrentUser } from './current-user.decorator';
import { skipAuth } from '@shared/helpers/skipAuth';
import { OwnershipGuard } from '@guards/authorization.guard';
import { AddCommentDto } from '@modules/comments/dto/add-comment.dto';
import { AddCommentDto } from '@modules/comments/dtos/add-comment.dto';
import { INVALID_ORG_ID, INVALID_PRODUCT_ID } from '@shared/constants/SystemMessages';
import { CustomHttpException } from '@shared/helpers/custom-http-filter';
import { SuperAdminGuard } from '@guards/super-admin.guard';
Expand Down Expand Up @@ -157,10 +157,9 @@ export class ProductsController {

@ApiBearerAuth()
@UseGuards(OwnershipGuard)
@Post('organisations/:productId/comments')
@ApiBearerAuth()
@Post('organisations/:orgId/products/:productId/comments')
@ApiOperation({ summary: 'Creates a comment for a product' })
@ApiParam({ name: 'id', description: 'organisation ID', example: '870ccb14-d6b0-4a50-b459-9895af803i89' })
@ApiParam({ name: 'orgId', description: 'organisation ID', example: '870ccb14-d6b0-4a50-b459-9895af803i89' })
@ApiParam({ name: 'productId', description: 'product ID', example: '126ccb14-d6b0-4a50-b459-9895af803h6y' })
@ApiBody({ type: AddCommentDto, description: 'Comment to be added' })
@ApiResponse({ status: 201, description: 'Comment added successfully' })
Expand All @@ -174,9 +173,9 @@ export class ProductsController {

@ApiBearerAuth()
@UseGuards(OwnershipGuard)
@Get('organisations/:productId/stock')
@Get('organisations/:orgId/products/:productId/stock')
@ApiOperation({ summary: 'Gets a product stock details by id' })
@ApiParam({ name: 'id', description: 'Organization ID', example: '12345' })
@ApiParam({ name: 'orgId', description: 'Organization ID', example: '12345' })
@ApiParam({ name: 'productId', description: 'Product ID' })
@ApiResponse({ status: 200, description: 'Product stock retrieved successfully' })
@ApiResponse({ status: 404, description: 'Product not found' })
Expand Down
68 changes: 13 additions & 55 deletions src/modules/products/products.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import {
ForbiddenException,
HttpStatus,
Injectable,
InternalServerErrorException,
Logger,
NotFoundException,
} from '@nestjs/common';
import { HttpStatus, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { endOfMonth, startOfMonth, subMonths } from 'date-fns';
import { Repository } from 'typeorm';
import * as SYS_MSG from '@shared/constants/SystemMessages';
import { AddCommentDto } from '../comments/dto/add-comment.dto';
import { Comment } from '../comments/entities/comments.entity';
import { Organisation } from '../organisations/entities/organisations.entity';
import { User } from '../user/entities/user.entity';
Expand All @@ -21,7 +13,7 @@ import { CreateReviewDto } from './dto/create-review.dto';
import { Review } from './entities/review.entity';
import { ProductResponseDto } from './dto/product-response.dto';
import { CustomHttpException } from '@shared/helpers/custom-http-filter';

import { AddCommentDto } from '../comments/dtos/add-comment.dto';

interface SearchCriteria {
name?: string;
Expand Down Expand Up @@ -63,32 +55,18 @@ export class ProductsService {
newProduct.org = org;
const statusCal = await this.calculateProductStatus(dto.quantity);
newProduct.stock_status = statusCal;
newProduct.cost_price = 0.2 * dto.price - dto.price;
newProduct.cost_price = dto.price - 0.2 * dto.price;
const product = await this.productRepository.save(newProduct);
if (!product || !newProduct)
throw new InternalServerErrorException({
status_code: 500,
status: 'Internal server error',
message: 'An unexpected error occurred. Please try again later.',
});

delete product.org;
return {
status: 'success',
status_code: HttpStatus.CREATED,
message: 'Product created successfully',
data: {
id: product.id,
name: product.name,
description: product.description,
price: product.price,
status: product.stock_status,
quantity: product.quantity,
created_at: product.created_at,
updated_at: product.updated_at,
},
data: product,
};
}

async getAllProducts({ page = 1, pageSize = 2 }: { page: number; pageSize: number }) {
async getAllProducts({ page = 1, pageSize = 10 }: { page: number; pageSize: number }) {
const skip = (page - 1) * pageSize;
const allProucts = await this.productRepository.find({ skip, take: pageSize });
const totalProducts = await this.productRepository.count();
Expand Down Expand Up @@ -140,13 +118,12 @@ export class ProductsService {
status_code: 422,
});

const { name, category, minPrice, maxPrice } = criteria;
const { name, minPrice, maxPrice } = criteria;
const query = this.productRepository.createQueryBuilder('product').where('product.orgId = :orgId', { orgId });

if (name) {
query.andWhere('product.name ILIKE :name', { name: `%${name}%` });
}

if (minPrice) {
query.andWhere('product.price >= :minPrice', { minPrice });
}
Expand All @@ -167,7 +144,7 @@ export class ProductsService {
return {
success: true,
statusCode: 200,
products,
data: products,
};
}

Expand All @@ -177,26 +154,9 @@ export class ProductsService {
}

async updateProduct(id: string, productId: string, updateProductDto: UpdateProductDTO) {
const org = await this.organisationRepository.findOne({ where: { id } });
if (!org)
throw new InternalServerErrorException({
status: 'Unprocessable entity exception',
message: 'Invalid organisation credentials',
status_code: 422,
});
const product = await this.productRepository.findOne({ where: { id: productId }, relations: ['org'] });
const product = await this.productRepository.findOne({ where: { id: productId, org: { id } } });
if (!product) {
throw new NotFoundException({
error: 'Product not found',
status_code: HttpStatus.NOT_FOUND,
});
}

if (product.org.id !== org.id) {
throw new ForbiddenException({
status: 'fail',
message: 'Not allowed to perform this action',
});
throw new NotFoundException('Invalid products credentials');
}

try {
Expand Down Expand Up @@ -254,15 +214,14 @@ export class ProductsService {
}

async addCommentToProduct(productId: string, commentDto: AddCommentDto, userId: string) {
const { comment } = commentDto;
const product = await this.productRepository.findOne({ where: { id: productId } });
const user = await this.userRepository.findOne({ where: { id: userId } });

if (!product) {
throw new CustomHttpException(SYS_MSG.PRODUCT_NOT_FOUND, HttpStatus.NOT_FOUND);
}

const productComment = this.commentRepository.create({ comment, product, user });
const productComment = this.commentRepository.create({ ...commentDto, product, user });

const saveComment = await this.commentRepository.save(productComment);

Expand Down Expand Up @@ -322,10 +281,9 @@ export class ProductsService {
const lastMonthEnds = this.getDateRange(lastMonth).monthEnds;

const totalProductsThisMonth = await this.getTotalProductsForDateRange(monthStarts, monthEnds);

const totalProductsLastMonth = await this.getTotalProductsForDateRange(lastMonthStarts, lastMonthEnds);

let percentageChange;
let percentageChange: string;

if (totalProductsLastMonth === totalProductsThisMonth) {
percentageChange =
Expand Down
Loading