Skip to content

Commit

Permalink
Merge pull request #1331 from PreciousIfeaka/fix/product-update
Browse files Browse the repository at this point in the history
fix: unauthorized error in product update, product stock data retrieval and comment addition
  • Loading branch information
incredible-phoenix246 authored Mar 1, 2025
2 parents fad19fc + fe54a29 commit d706e90
Show file tree
Hide file tree
Showing 12 changed files with 105 additions and 119 deletions.
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
File renamed without changes.
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

0 comments on commit d706e90

Please sign in to comment.