From 5da63041c2b93762ea28049e5b4960a7e0235013 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sat, 10 May 2025 11:49:58 -0400 Subject: [PATCH 01/75] Update websocket to only notify when the order is updated --- src/order/order.gateway.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/order/order.gateway.ts b/src/order/order.gateway.ts index cad60da..4b5773a 100644 --- a/src/order/order.gateway.ts +++ b/src/order/order.gateway.ts @@ -60,16 +60,12 @@ export class OrderGateway implements OnGatewayConnection, OnGatewayDisconnect { @ConnectedSocket() client: Socket, @MessageBody() data: UpdateOrderStatusWsDTO, ) { - this.orderService.update(data.id, data.status).then((order) => { - if (!order) { - this.server.to(client.id).emit('error', { - message: 'Order not found', - data: { id: data.id }, - }); - } + this.orderService.findOneWithUser(data.id).then((order) => { this.orderService.getUserByOrderId(order.id).then((user) => { if (user.wsId) { - client.to(user.wsId).emit('order', order); + client + .to(user.wsId) + .emit('orderUpdated', { orderId: order.id, status: data.status }); } }); }); From 3e3e629fad2fa910bb05ed505c369b835e25fd50 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sat, 10 May 2025 11:53:53 -0400 Subject: [PATCH 02/75] Create socket to notify when the delivery is updated --- src/order/dto/order-delivery.dto.ts | 18 ++++++++++++++++++ src/order/order.gateway.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/order/dto/order-delivery.dto.ts b/src/order/dto/order-delivery.dto.ts index d0fee0b..c0156e5 100644 --- a/src/order/dto/order-delivery.dto.ts +++ b/src/order/dto/order-delivery.dto.ts @@ -162,3 +162,21 @@ export class OrderDeliveryEmployeeDTO extends IntersectionType( @Type(() => EmployeeDTO) employee: EmployeeDTO; } + +export class UpdateDeliveryWsDTO { + @ApiProperty({ description: 'ID of the order delivery' }) + @IsString() + @IsUUID() + id: string; + + @ApiPropertyOptional({ description: 'New delivery status' }) + @IsOptional() + @IsString() + deliveryStatus?: string; + + @ApiPropertyOptional({ + description: 'indicate the delivery is rejected (employee unassign)', + }) + @IsOptional() + employeeId?: string; +} diff --git a/src/order/order.gateway.ts b/src/order/order.gateway.ts index 4b5773a..dc64c88 100644 --- a/src/order/order.gateway.ts +++ b/src/order/order.gateway.ts @@ -23,6 +23,7 @@ import { RolesGuardWs } from 'src/auth/roles.guard'; import { Roles } from 'src/auth/roles.decorador'; import { UserRole } from 'src/user/entities/user.entity'; import { WebsocketExceptionsFilter } from './ws.filters'; +import { UpdateDeliveryWsDTO } from './dto/order-delivery.dto'; @WebSocketGateway({ cors: { @@ -70,4 +71,30 @@ export class OrderGateway implements OnGatewayConnection, OnGatewayDisconnect { }); }); } + + @UseFilters(new WebsocketExceptionsFilter()) + @UsePipes( + new ValidationPipe({ + exceptionFactory: (errors) => new WsException(errors), + }), + ) + @UseGuards(AuthGuardWs, RolesGuardWs) + @Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) + @SubscribeMessage('updateDelivery') + updateDelivery( + @ConnectedSocket() client: Socket, + @MessageBody() data: UpdateDeliveryWsDTO, + ) { + this.orderService.getDelivery(data.id).then((delivery) => { + this.orderService.getUserByOrderId(delivery.order.id).then((user) => { + if (user.wsId) { + client.to(user.wsId).emit('deliveryUpdated', { + orderDeliveryId: delivery.id, + status: data.deliveryStatus, + employeeId: delivery.employee.id, + }); + } + }); + }); + } } From 08952ae26f9f119c53f883baae9865cd86100797 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sat, 10 May 2025 23:45:50 -0400 Subject: [PATCH 03/75] add sales prediction model --- src/app.module.ts | 2 ++ src/sales/sales.controller.ts | 13 ++++++++ src/sales/sales.module.ts | 12 +++++++ src/sales/sales.service.ts | 61 +++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 src/sales/sales.controller.ts create mode 100644 src/sales/sales.module.ts create mode 100644 src/sales/sales.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index a23fa01..d63fa27 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,6 +22,7 @@ import { CartModule } from './cart/cart.module'; import { RecommendationModule } from './recommendation/recommendation.module'; import { ReportsModule } from './reports/reports.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; +import { SalesModule } from './sales/sales.module'; @Module({ imports: [ @@ -67,6 +68,7 @@ import { EventEmitterModule } from '@nestjs/event-emitter'; CartModule, RecommendationModule, ReportsModule, + SalesModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/sales/sales.controller.ts b/src/sales/sales.controller.ts new file mode 100644 index 0000000..22d2418 --- /dev/null +++ b/src/sales/sales.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { SalesService } from './sales.service'; + +@Controller('sales') +export class SalesController { + constructor(private readonly salesService: SalesService) {} + + @Get('predict') + async predict(@Query('days') days: string) { + const daysAhead = parseInt(days, 10) || 7; + return await this.salesService.predictNext(daysAhead); + } +} diff --git a/src/sales/sales.module.ts b/src/sales/sales.module.ts new file mode 100644 index 0000000..694b680 --- /dev/null +++ b/src/sales/sales.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Order } from 'src/order/entities/order.entity'; +import { SalesController } from './sales.controller'; +import { SalesService } from './sales.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Order])], + controllers: [SalesController], + providers: [SalesService], +}) +export class SalesModule {} diff --git a/src/sales/sales.service.ts b/src/sales/sales.service.ts new file mode 100644 index 0000000..1dfd28e --- /dev/null +++ b/src/sales/sales.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Order, OrderStatus } from '../order/entities/order.entity'; +import { Repository } from 'typeorm'; +import * as tf from '@tensorflow/tfjs'; +@Injectable() +export class SalesService { + constructor( + @InjectRepository(Order) + private readonly orderRepository: Repository, + ) {} + + async getDailySales(): Promise<{ date: string; total: number }[]> { + const sales = await this.orderRepository + .createQueryBuilder('order') + .select('DATE(order.createdAt)', 'date') + .addSelect('SUM(order.totalPrice)', 'total') + .where('order.status = :status', { status: OrderStatus.COMPLETED }) + .groupBy('DATE(order.createdAt)') + .orderBy('DATE(order.createdAt)', 'ASC') + .getRawMany<{ date: string; total: string }>(); + + return sales.map((row) => ({ + date: row.date, + total: parseInt(row.total), + })); + } + + async predictNext(daysAhead: number = 7) { + const salesData = await this.getDailySales(); + if (salesData.length < 2) return []; + + const inputs = salesData.map((_, i) => i); + const labels = salesData.map((s) => s.total); + + const xs = tf.tensor2d(inputs, [inputs.length, 1]); + const ys = tf.tensor2d(labels, [labels.length, 1]); + + const model = tf.sequential(); + model.add(tf.layers.dense({ inputShape: [1], units: 1 })); + model.compile({ optimizer: 'sgd', loss: 'meanSquaredError' }); + + await model.fit(xs, ys, { epochs: 300 }); + + const futureDays = Array.from( + { length: daysAhead }, + (_, i) => inputs.length + i, + ); + const predictions = model.predict( + tf.tensor2d(futureDays, [daysAhead, 1]), + ) as tf.Tensor; + + const predictedValues = await predictions.array(); + return futureDays.map((i, index) => ({ + dayIndex: i, + predictedTotal: parseInt( + (predictedValues as number[][])[index][0].toFixed(2), + ), + })); + } +} From 89b400492770c41856bbdf849edb6e40faf630cb Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 17:15:05 -0400 Subject: [PATCH 04/75] add days without orders to the model --- src/sales/sales.service.ts | 52 +++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/sales/sales.service.ts b/src/sales/sales.service.ts index 1dfd28e..733dd52 100644 --- a/src/sales/sales.service.ts +++ b/src/sales/sales.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Order, OrderStatus } from '../order/entities/order.entity'; import { Repository } from 'typeorm'; import * as tf from '@tensorflow/tfjs'; + @Injectable() export class SalesService { constructor( @@ -26,8 +27,35 @@ export class SalesService { })); } + fillMissingDates(data: { date: string; total: number }[]) { + if (data.length < 2) return data; + + const filled = []; + const start = new Date(data[0].date); + const yesterday = new Date(); + yesterday.setHours(0, 0, 0, 0); + yesterday.setDate(yesterday.getDate() - 1); + + const dateMap = new Map( + data.map((d) => [new Date(d.date).toISOString().split('T')[0], d.total]), + ); + + for (let d = new Date(start); d <= yesterday; d.setDate(d.getDate() + 1)) { + const dateStr = d.toISOString().split('T')[0]; + + filled.push({ + date: dateStr, + total: dateMap.get(dateStr) ?? 0, + }); + } + + return filled; + } + async predictNext(daysAhead: number = 7) { - const salesData = await this.getDailySales(); + const rawSalesData = await this.getDailySales(); + const salesData = this.fillMissingDates(rawSalesData); + if (salesData.length < 2) return []; const inputs = salesData.map((_, i) => i); @@ -46,16 +74,26 @@ export class SalesService { { length: daysAhead }, (_, i) => inputs.length + i, ); + const predictions = model.predict( tf.tensor2d(futureDays, [daysAhead, 1]), ) as tf.Tensor; const predictedValues = await predictions.array(); - return futureDays.map((i, index) => ({ - dayIndex: i, - predictedTotal: parseInt( - (predictedValues as number[][])[index][0].toFixed(2), - ), - })); + + return futureDays.map((_, index) => { + const date = new Date(); + date.setHours(0, 0, 0, 0); + date.setDate(date.getDate() + index); + + const formatted = date.toISOString().split('T')[0]; + + return { + date: formatted, + predictedTotal: parseInt( + (predictedValues as number[][])[index][0].toFixed(2), + ), + }; + }); } } From 282791fbcbc974dcfdb2f939089a8c377182b5c6 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 17:36:57 -0400 Subject: [PATCH 05/75] add DTOs --- src/sales/dto/predict-sales.dto.ts | 18 ++++++++++++++++++ src/sales/sales.controller.ts | 6 +++--- src/sales/sales.service.ts | 7 ++++--- 3 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 src/sales/dto/predict-sales.dto.ts diff --git a/src/sales/dto/predict-sales.dto.ts b/src/sales/dto/predict-sales.dto.ts new file mode 100644 index 0000000..53587d4 --- /dev/null +++ b/src/sales/dto/predict-sales.dto.ts @@ -0,0 +1,18 @@ +import { IsInt, IsOptional, Min } from 'class-validator'; + +export class PredictSalesDTO { + @IsOptional() + @IsInt() + @Min(1) + daysAhead?: number = 7; +} + +export class DailySaleDTO { + date: string; + total: number; +} + +export class PredictedSaleDTO { + date: string; + predictedTotal: number; +} diff --git a/src/sales/sales.controller.ts b/src/sales/sales.controller.ts index 22d2418..ce6e36e 100644 --- a/src/sales/sales.controller.ts +++ b/src/sales/sales.controller.ts @@ -1,13 +1,13 @@ import { Controller, Get, Query } from '@nestjs/common'; import { SalesService } from './sales.service'; +import { PredictedSaleDTO, PredictSalesDTO } from './dto/predict-sales.dto'; @Controller('sales') export class SalesController { constructor(private readonly salesService: SalesService) {} @Get('predict') - async predict(@Query('days') days: string) { - const daysAhead = parseInt(days, 10) || 7; - return await this.salesService.predictNext(daysAhead); + async predict(@Query() query: PredictSalesDTO): Promise { + return await this.salesService.predictNext(query.daysAhead || 7); } } diff --git a/src/sales/sales.service.ts b/src/sales/sales.service.ts index 733dd52..24e5d4c 100644 --- a/src/sales/sales.service.ts +++ b/src/sales/sales.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Order, OrderStatus } from '../order/entities/order.entity'; import { Repository } from 'typeorm'; import * as tf from '@tensorflow/tfjs'; +import { DailySaleDTO, PredictedSaleDTO } from './dto/predict-sales.dto'; @Injectable() export class SalesService { @@ -11,7 +12,7 @@ export class SalesService { private readonly orderRepository: Repository, ) {} - async getDailySales(): Promise<{ date: string; total: number }[]> { + async getDailySales(): Promise { const sales = await this.orderRepository .createQueryBuilder('order') .select('DATE(order.createdAt)', 'date') @@ -27,7 +28,7 @@ export class SalesService { })); } - fillMissingDates(data: { date: string; total: number }[]) { + fillMissingDates(data: DailySaleDTO[]): DailySaleDTO[] { if (data.length < 2) return data; const filled = []; @@ -52,7 +53,7 @@ export class SalesService { return filled; } - async predictNext(daysAhead: number = 7) { + async predictNext(daysAhead: number = 7): Promise { const rawSalesData = await this.getDailySales(); const salesData = this.fillMissingDates(rawSalesData); From 0721f388618f458a8f31457b4d103ee1c9ed2f4f Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 17:49:30 -0400 Subject: [PATCH 06/75] refactor sales service --- src/sales/sales.controller.ts | 8 +++++++- src/sales/sales.service.ts | 16 ++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/sales/sales.controller.ts b/src/sales/sales.controller.ts index ce6e36e..588f61d 100644 --- a/src/sales/sales.controller.ts +++ b/src/sales/sales.controller.ts @@ -8,6 +8,12 @@ export class SalesController { @Get('predict') async predict(@Query() query: PredictSalesDTO): Promise { - return await this.salesService.predictNext(query.daysAhead || 7); + const salesData = await this.salesService.getDailySales(); + const dailySales = this.salesService.fillMissingDates(salesData); + + return await this.salesService.predictNext( + query.daysAhead || 7, + dailySales, + ); } } diff --git a/src/sales/sales.service.ts b/src/sales/sales.service.ts index 24e5d4c..bad7072 100644 --- a/src/sales/sales.service.ts +++ b/src/sales/sales.service.ts @@ -53,14 +53,14 @@ export class SalesService { return filled; } - async predictNext(daysAhead: number = 7): Promise { - const rawSalesData = await this.getDailySales(); - const salesData = this.fillMissingDates(rawSalesData); - - if (salesData.length < 2) return []; - - const inputs = salesData.map((_, i) => i); - const labels = salesData.map((s) => s.total); + async predictNext( + daysAhead: number = 7, + dailySales: DailySaleDTO[], + ): Promise { + if (dailySales.length < 2) return []; + + const inputs = dailySales.map((_, i) => i); + const labels = dailySales.map((s) => s.total); const xs = tf.tensor2d(inputs, [inputs.length, 1]); const ys = tf.tensor2d(labels, [labels.length, 1]); From 42a03e46cfa3d1f1acba094a8dc7b6c105be74cb Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 18:01:34 -0400 Subject: [PATCH 07/75] restrict endpoint access to ADMIN and BRANCH_ADMIN roles --- src/sales/sales.controller.ts | 8 +++++++- src/sales/sales.module.ts | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/sales/sales.controller.ts b/src/sales/sales.controller.ts index 588f61d..b7b0279 100644 --- a/src/sales/sales.controller.ts +++ b/src/sales/sales.controller.ts @@ -1,11 +1,17 @@ -import { Controller, Get, Query } from '@nestjs/common'; +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; import { SalesService } from './sales.service'; import { PredictedSaleDTO, PredictSalesDTO } from './dto/predict-sales.dto'; +import { AuthGuard } from 'src/auth/auth.guard'; +import { RolesGuard } from 'src/auth/roles.guard'; +import { Roles } from 'src/auth/roles.decorador'; +import { UserRole } from 'src/user/entities/user.entity'; @Controller('sales') export class SalesController { constructor(private readonly salesService: SalesService) {} + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) @Get('predict') async predict(@Query() query: PredictSalesDTO): Promise { const salesData = await this.salesService.getDailySales(); diff --git a/src/sales/sales.module.ts b/src/sales/sales.module.ts index 694b680..193092b 100644 --- a/src/sales/sales.module.ts +++ b/src/sales/sales.module.ts @@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Order } from 'src/order/entities/order.entity'; import { SalesController } from './sales.controller'; import { SalesService } from './sales.service'; +import { AuthModule } from 'src/auth/auth.module'; @Module({ - imports: [TypeOrmModule.forFeature([Order])], + imports: [TypeOrmModule.forFeature([Order]), AuthModule], controllers: [SalesController], providers: [SalesService], }) From bb28006fca96586933868f18c562ce1e2ba6940e Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 18:25:40 -0400 Subject: [PATCH 08/75] add swagger documentation --- src/sales/dto/predict-sales.dto.ts | 7 ++++++- src/sales/sales.controller.ts | 29 +++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/sales/dto/predict-sales.dto.ts b/src/sales/dto/predict-sales.dto.ts index 53587d4..bdbb87e 100644 --- a/src/sales/dto/predict-sales.dto.ts +++ b/src/sales/dto/predict-sales.dto.ts @@ -1,10 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsInt, IsOptional, Min } from 'class-validator'; export class PredictSalesDTO { @IsOptional() @IsInt() @Min(1) - daysAhead?: number = 7; + days?: number = 7; } export class DailySaleDTO { @@ -13,6 +14,10 @@ export class DailySaleDTO { } export class PredictedSaleDTO { + @ApiProperty() date: string; + + @ApiProperty() + @IsInt() predictedTotal: number; } diff --git a/src/sales/sales.controller.ts b/src/sales/sales.controller.ts index b7b0279..fd07b0d 100644 --- a/src/sales/sales.controller.ts +++ b/src/sales/sales.controller.ts @@ -5,6 +5,12 @@ import { AuthGuard } from 'src/auth/auth.guard'; import { RolesGuard } from 'src/auth/roles.guard'; import { Roles } from 'src/auth/roles.decorador'; import { UserRole } from 'src/user/entities/user.entity'; +import { + ApiBearerAuth, + ApiOperation, + ApiQuery, + ApiResponse, +} from '@nestjs/swagger'; @Controller('sales') export class SalesController { @@ -13,13 +19,28 @@ export class SalesController { @UseGuards(AuthGuard, RolesGuard) @Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) @Get('predict') + @ApiBearerAuth() + @ApiOperation({ + summary: 'Predict future daily sales', + description: + 'Returns sales predictions starting from today for the number of future days specified.', + }) + @ApiQuery({ + name: 'days', + required: false, + description: 'Number of future days to predict (default: 7)', + type: Number, + example: 5, + }) + @ApiResponse({ + status: 200, + description: 'List of predicted daily sales values', + type: [PredictedSaleDTO], + }) async predict(@Query() query: PredictSalesDTO): Promise { const salesData = await this.salesService.getDailySales(); const dailySales = this.salesService.fillMissingDates(salesData); - return await this.salesService.predictNext( - query.daysAhead || 7, - dailySales, - ); + return await this.salesService.predictNext(query.days || 7, dailySales); } } From d602559eb79336a8e1f662da4c4d45d0c92b6171 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 18:40:14 -0400 Subject: [PATCH 09/75] fix query optional --- src/sales/dto/predict-sales.dto.ts | 6 ++---- src/sales/sales.controller.ts | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/sales/dto/predict-sales.dto.ts b/src/sales/dto/predict-sales.dto.ts index bdbb87e..1dbfeb5 100644 --- a/src/sales/dto/predict-sales.dto.ts +++ b/src/sales/dto/predict-sales.dto.ts @@ -1,11 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsOptional, Min } from 'class-validator'; +import { IsInt, IsOptional } from 'class-validator'; export class PredictSalesDTO { @IsOptional() - @IsInt() - @Min(1) - days?: number = 7; + days?: string = '7'; } export class DailySaleDTO { diff --git a/src/sales/sales.controller.ts b/src/sales/sales.controller.ts index fd07b0d..5fd1472 100644 --- a/src/sales/sales.controller.ts +++ b/src/sales/sales.controller.ts @@ -41,6 +41,7 @@ export class SalesController { const salesData = await this.salesService.getDailySales(); const dailySales = this.salesService.fillMissingDates(salesData); - return await this.salesService.predictNext(query.days || 7, dailySales); + const days = query.days ? parseInt(query.days, 10) || 7 : 7; + return await this.salesService.predictNext(days, dailySales); } } From 86ff849fea82d050d58ed5f72c9e062d196715d4 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 11 May 2025 20:22:41 -0400 Subject: [PATCH 10/75] Add promo type to product dto --- src/products/dto/product.dto.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/products/dto/product.dto.ts b/src/products/dto/product.dto.ts index 940bf17..deb1532 100644 --- a/src/products/dto/product.dto.ts +++ b/src/products/dto/product.dto.ts @@ -12,6 +12,7 @@ import { CategoryResponseDTO } from 'src/category/dto/category.dto'; import { ResponsePresentationDTO } from './presentation.dto'; import { PaginationQueryDTO } from 'src/utils/dto/pagination.dto'; import { Expose, Transform, Type } from 'class-transformer'; +import { ResponsePromoDTO } from 'src/discount/dto/promo.dto'; export class AddCategoryDTO { @IsString() @@ -83,6 +84,11 @@ export class ProductPresentationDTO extends BaseDTO { @Type(() => ProductDTO) product: ProductDTO; + @Expose() + @Type(() => ResponsePromoDTO) + @ApiProperty({ type: ResponsePromoDTO }) + promo: ResponsePromoDTO; + @Expose() @ApiProperty({ description: 'Stock quantity in all branches' }) stock: number; From 5e5b3a94d8767c833a68c086ceb2b98cca0869fd Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 11 May 2025 20:23:02 -0400 Subject: [PATCH 11/75] Add left join with promo --- src/products/products.service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/products/products.service.ts b/src/products/products.service.ts index d01381e..2281a8c 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -113,6 +113,15 @@ export class ProductsService { categories: 'product.categories', presentation: 'product_presentation.presentation', }, + leftJoinAndSelect: { + promo: 'product_presentation.promo', + }, + }, + order: { + createdAt: 'DESC', + product: { + priority: 'ASC', + }, }, where, skip: (page - 1) * limit, From 9d3b88a5d0b3e6f6a926466df992110629e0e0da Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 22:08:45 -0400 Subject: [PATCH 12/75] add migration to enable PostGIS extension --- ...14730112-enable-postgis-extension-migration.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/database/migrations/1747014730112-enable-postgis-extension-migration.ts diff --git a/src/database/migrations/1747014730112-enable-postgis-extension-migration.ts b/src/database/migrations/1747014730112-enable-postgis-extension-migration.ts new file mode 100644 index 0000000..3df47ae --- /dev/null +++ b/src/database/migrations/1747014730112-enable-postgis-extension-migration.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class EnablePostgisExtensionMigration1747014730112 + implements MigrationInterface +{ + name = 'EnablePostgisExtensionMigration1747014730112'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS postgis`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP EXTENSION IF EXISTS postgis`); + } +} From 8e9a4d11316f1d7ab8d0c984c9e1e922154e69e2 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 22:53:36 -0400 Subject: [PATCH 13/75] assign nearest branch on delivery using PostGIS --- src/branch/branch.service.ts | 16 ++++++++++++++++ src/order/order.service.ts | 8 +++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/branch/branch.service.ts b/src/branch/branch.service.ts index df8d5c0..d496727 100644 --- a/src/branch/branch.service.ts +++ b/src/branch/branch.service.ts @@ -93,4 +93,20 @@ export class BranchService { }); return deleted.affected === 1; } + + async findNearestBranch(lat: number, lng: number): Promise { + const branch = await this.branchRepository + .createQueryBuilder('branch') + .orderBy( + `ST_DistanceSphere(ST_MakePoint(branch.longitude, branch.latitude), ST_MakePoint(:lng, :lat))`, + ) + .setParameters({ lat, lng }) + .getOne(); + + if (!branch) { + throw new NotFoundException('No nearest branch found'); + } + + return branch; + } } diff --git a/src/order/order.service.ts b/src/order/order.service.ts index 868bc6a..da50598 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -66,13 +66,15 @@ export class OrderService { 'User address ID is required for delivery orders', ); } - // TODO: Find the closest branch to the address - const branches = await this.branchService.findAll(1, 10); - branch = branches[0]; userAddress = await this.userService.getAddress( user.id, createOrderDTO.userAddressId, ); + const nearestBranch = await this.branchService.findNearestBranch( + userAddress.latitude, + userAddress.longitude, + ); + branch = nearestBranch; } else { throw new BadRequestException('Invalid order type'); } From 86450b1c366ef7f49c6728fdf48065e2e5ecc007 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 23:12:34 -0400 Subject: [PATCH 14/75] fix NotFoundException message --- src/branch/branch.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/branch/branch.service.ts b/src/branch/branch.service.ts index d496727..0fd9f56 100644 --- a/src/branch/branch.service.ts +++ b/src/branch/branch.service.ts @@ -104,7 +104,7 @@ export class BranchService { .getOne(); if (!branch) { - throw new NotFoundException('No nearest branch found'); + throw new NotFoundException('No closer branch found'); } return branch; From 2b8a497fbb73b99b3ead037bccdcc9efd84e8234 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 23:52:57 -0400 Subject: [PATCH 15/75] validate user before confirming order payment --- src/order/order.service.ts | 1 + .../payment-confirmation.controller.ts | 18 +++++++++++++++--- .../services/payment-confirmation.service.ts | 10 +++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/order/order.service.ts b/src/order/order.service.ts index 868bc6a..a218791 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -205,6 +205,7 @@ export class OrderService { where: where, relations: [ 'branch', + 'user', 'details', 'details.productPresentation', 'details.productPresentation.promo', diff --git a/src/payments/controllers/payment-confirmation.controller.ts b/src/payments/controllers/payment-confirmation.controller.ts index 68d5547..9d668c5 100644 --- a/src/payments/controllers/payment-confirmation.controller.ts +++ b/src/payments/controllers/payment-confirmation.controller.ts @@ -1,10 +1,16 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common'; import { PaymentConfirmationService } from '../services/payment-confirmation.service'; import { CreatePaymentConfirmationDTO, ResponsePaymentConfirmationDTO, } from '../dto/payment-confirmation.dto'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + ApiBearerAuth, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { AuthGuard, CustomRequest } from 'src/auth/auth.guard'; @ApiTags('Payment Confirmation') @Controller('payment-confirmation') @@ -14,6 +20,8 @@ export class PaymentConfirmationController { ) {} @Post() + @UseGuards(AuthGuard) + @ApiBearerAuth() @ApiOperation({ summary: 'Create Payment Confirmation', description: 'Registers a new payment confirmation for a user order.', @@ -24,8 +32,12 @@ export class PaymentConfirmationController { type: ResponsePaymentConfirmationDTO, }) async create( + @Req() req: CustomRequest, @Body() createPaymentConfirmationDto: CreatePaymentConfirmationDTO, ): Promise { - return this.paymentConfirmationService.create(createPaymentConfirmationDto); + return this.paymentConfirmationService.create( + req.user.id, + createPaymentConfirmationDto, + ); } } diff --git a/src/payments/services/payment-confirmation.service.ts b/src/payments/services/payment-confirmation.service.ts index ee12094..3d4d640 100644 --- a/src/payments/services/payment-confirmation.service.ts +++ b/src/payments/services/payment-confirmation.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { ForbiddenException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { PaymentConfirmation } from '../entities/payment-confirmation.entity'; @@ -14,11 +14,19 @@ export class PaymentConfirmationService { ) {} async create( + userId: string, createPaymentConfirmationDto: CreatePaymentConfirmationDTO, ): Promise { const order = await this.orderService.findOne( createPaymentConfirmationDto.orderId, ); + console.log('order', order); + + if (order.user.id !== userId) { + throw new ForbiddenException( + 'You are not allowed to confirm this payment', + ); + } const confirmation = this.paymentConfirmationRepository.create({ ...createPaymentConfirmationDto, order, From 81d1f8c54c682ca6b9f47fac3394bb7be6477385 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sun, 11 May 2025 23:57:13 -0400 Subject: [PATCH 16/75] add swagger documentation --- .../controllers/payment-confirmation.controller.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/payments/controllers/payment-confirmation.controller.ts b/src/payments/controllers/payment-confirmation.controller.ts index 9d668c5..44cf789 100644 --- a/src/payments/controllers/payment-confirmation.controller.ts +++ b/src/payments/controllers/payment-confirmation.controller.ts @@ -6,9 +6,11 @@ import { } from '../dto/payment-confirmation.dto'; import { ApiBearerAuth, + ApiForbiddenResponse, ApiOperation, ApiResponse, ApiTags, + ApiUnauthorizedResponse, } from '@nestjs/swagger'; import { AuthGuard, CustomRequest } from 'src/auth/auth.guard'; @@ -31,6 +33,13 @@ export class PaymentConfirmationController { description: 'Payment confirmation created successfully.', type: ResponsePaymentConfirmationDTO, }) + @ApiUnauthorizedResponse({ + description: 'Unauthorized - Missing or invalid JWT token.', + }) + @ApiForbiddenResponse({ + description: + 'Forbidden - The order does not belong to the authenticated user.', + }) async create( @Req() req: CustomRequest, @Body() createPaymentConfirmationDto: CreatePaymentConfirmationDTO, From e3ba38fa173321184c54274f9b0600667696ad5c Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Mon, 12 May 2025 00:10:56 -0400 Subject: [PATCH 17/75] remove console.log from payment confirmation service --- src/payments/services/payment-confirmation.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/payments/services/payment-confirmation.service.ts b/src/payments/services/payment-confirmation.service.ts index 3d4d640..2d09a14 100644 --- a/src/payments/services/payment-confirmation.service.ts +++ b/src/payments/services/payment-confirmation.service.ts @@ -20,7 +20,6 @@ export class PaymentConfirmationService { const order = await this.orderService.findOne( createPaymentConfirmationDto.orderId, ); - console.log('order', order); if (order.user.id !== userId) { throw new ForbiddenException( From cf6138a940edcc5db307bb42472b2c9af8fc1075 Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Mon, 12 May 2025 20:15:07 -0400 Subject: [PATCH 18/75] add SSE --- src/database/database.module.ts | 1 - .../controllers/notification.controller.ts | 14 ++++++++++++++ .../services/notification.service.ts | 18 ++++++++++++++++++ .../subscribers/notification.subscriber.ts | 17 ++++++++++++++++- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 0cfd744..ec76f28 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -15,7 +15,6 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; password: configService.get('POSTGRES_PASSWORD'), database: configService.get('POSTGRES_NAME'), entities: [__dirname + '/../**/*.entity{.ts,.js}'], - subscribers: [__dirname + '/../**/*.subscriber{.ts,.js}'], synchronize: false, ssl: configService.get('POSTGRES_SSL', false), }), diff --git a/src/notification/controllers/notification.controller.ts b/src/notification/controllers/notification.controller.ts index 88b6ef4..8cb5846 100644 --- a/src/notification/controllers/notification.controller.ts +++ b/src/notification/controllers/notification.controller.ts @@ -8,11 +8,14 @@ import { HttpCode, HttpStatus, ParseUUIDPipe, + Sse, + MessageEvent, } from '@nestjs/common'; import { NotificationService } from '../services/notification.service'; import { AuthGuard, CustomRequest } from 'src/auth/auth.guard'; import { ApiBearerAuth, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { UserRole } from 'src/user/entities/user.entity'; +import { Observable } from 'rxjs'; @Controller('notification') export class NotificationController { @@ -48,4 +51,15 @@ export class NotificationController { ): Promise { await this.notificationService.markAsReadAsCustomer(orderId, req.user.id); } + @Sse('stream') + @UseGuards(AuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'SSE real-time notification flow' }) + @ApiResponse({ + status: 200, + description: 'Notification SSE Event Stream', + }) + streamNotifications(@Req() req: CustomRequest): Observable { + return this.notificationService.subscribeToNotifications(req.user.id); + } } diff --git a/src/notification/services/notification.service.ts b/src/notification/services/notification.service.ts index 0bda105..39f923a 100644 --- a/src/notification/services/notification.service.ts +++ b/src/notification/services/notification.service.ts @@ -2,6 +2,8 @@ import { Injectable, ForbiddenException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Notification } from '../entities/notification.entity'; +import { Subject, Observable, filter } from 'rxjs'; +import { MessageEvent } from '@nestjs/common'; @Injectable() export class NotificationService { @@ -9,6 +11,7 @@ export class NotificationService { @InjectRepository(Notification) private readonly notificationRepository: Repository, ) {} + private notifications$ = new Subject(); async getAllNotifications() { return await this.notificationRepository.find({ @@ -38,4 +41,19 @@ export class NotificationService { notification.isRead = true; await this.notificationRepository.save(notification); } + + emitNotification(notification: Notification) { + const event: MessageEvent = { + data: notification.message, + id: notification.order.user.id, + type: 'notification', + }; + this.notifications$.next(event); + } + + subscribeToNotifications(userId: string): Observable { + return this.notifications$ + .asObservable() + .pipe(filter((evt) => evt.id === userId)); + } } diff --git a/src/notification/subscribers/notification.subscriber.ts b/src/notification/subscribers/notification.subscriber.ts index 4579973..80ff90e 100644 --- a/src/notification/subscribers/notification.subscriber.ts +++ b/src/notification/subscribers/notification.subscriber.ts @@ -2,14 +2,24 @@ import { EntitySubscriberInterface, EventSubscriber, UpdateEvent, + DataSource, } from 'typeorm'; import { Order, OrderStatus } from 'src/order/entities/order.entity'; import { Notification } from '../entities/notification.entity'; +import { NotificationService } from '../services/notification.service'; +import { Injectable } from '@nestjs/common'; @EventSubscriber() +@Injectable() export class NotificationSubscriber implements EntitySubscriberInterface { + constructor( + private readonly dataSource: DataSource, + private readonly notificationService: NotificationService, + ) { + this.dataSource.subscribers.push(this); + } listenTo() { return Order; } @@ -57,6 +67,11 @@ export class NotificationSubscriber }); } - await notificationRepository.save(notification); + const saved = await notificationRepository.save(notification); + const fullNotification = await notificationRepository.findOneOrFail({ + where: { id: saved.id }, + relations: ['order', 'order.user'], + }); + this.notificationService.emitNotification(fullNotification); } } From 2febeab41dc6298513e663a9b1d30ae787d2dd0e Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Mon, 12 May 2025 22:08:01 -0400 Subject: [PATCH 19/75] change findOne to findOneWithUser --- src/order/order.service.ts | 1 - src/payments/services/payment-confirmation.service.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/order/order.service.ts b/src/order/order.service.ts index a218791..868bc6a 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -205,7 +205,6 @@ export class OrderService { where: where, relations: [ 'branch', - 'user', 'details', 'details.productPresentation', 'details.productPresentation.promo', diff --git a/src/payments/services/payment-confirmation.service.ts b/src/payments/services/payment-confirmation.service.ts index 2d09a14..dabc264 100644 --- a/src/payments/services/payment-confirmation.service.ts +++ b/src/payments/services/payment-confirmation.service.ts @@ -17,7 +17,7 @@ export class PaymentConfirmationService { userId: string, createPaymentConfirmationDto: CreatePaymentConfirmationDTO, ): Promise { - const order = await this.orderService.findOne( + const order = await this.orderService.findOneWithUser( createPaymentConfirmationDto.orderId, ); From d1a60be3a49dee94c0c73dfa26790a096204b7a2 Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Mon, 12 May 2025 23:05:26 -0400 Subject: [PATCH 20/75] add price --- src/order/entities/order.entity.ts | 3 +++ ...01623045-add-price-order-detail-migration.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/order/migrations/1747101623045-add-price-order-detail-migration.ts diff --git a/src/order/entities/order.entity.ts b/src/order/entities/order.entity.ts index 47a05b0..d46698e 100644 --- a/src/order/entities/order.entity.ts +++ b/src/order/entities/order.entity.ts @@ -83,6 +83,9 @@ export class OrderDetail extends UUIDModel { @Column({ type: 'int', name: 'quantity' }) quantity: number; + @Column({ type: 'int', name: 'price' }) + price: number; + @Column({ type: 'int', name: 'subtotal' }) subtotal: number; diff --git a/src/order/migrations/1747101623045-add-price-order-detail-migration.ts b/src/order/migrations/1747101623045-add-price-order-detail-migration.ts new file mode 100644 index 0000000..13df57d --- /dev/null +++ b/src/order/migrations/1747101623045-add-price-order-detail-migration.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPriceOrderDetailMigration1747101623045 + implements MigrationInterface +{ + name = 'AddPriceOrderDetailMigration1747101623045'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "order_detail" ADD "price" integer NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "order_detail" DROP COLUMN "price"`); + } +} From 8c6d7959638a15adc50b645ce18c04a20727c11b Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Tue, 13 May 2025 00:24:53 -0400 Subject: [PATCH 21/75] add active principle --- .../active-principle.controller.ts | 66 +++++++++++++++++++ .../active-principle.module.ts | 13 ++++ .../active-principle.service.ts | 30 +++++++++ .../dto/active-principle.dto.ts | 10 +++ .../entities/active-principle.entity.ts | 8 +++ ...07651171-add-active-principle-migration.ts | 17 +++++ src/app.module.ts | 2 + 7 files changed, 146 insertions(+) create mode 100644 src/active-principle/active-principle.controller.ts create mode 100644 src/active-principle/active-principle.module.ts create mode 100644 src/active-principle/active-principle.service.ts create mode 100644 src/active-principle/dto/active-principle.dto.ts create mode 100644 src/active-principle/entities/active-principle.entity.ts create mode 100644 src/active-principle/migrations/1747107651171-add-active-principle-migration.ts diff --git a/src/active-principle/active-principle.controller.ts b/src/active-principle/active-principle.controller.ts new file mode 100644 index 0000000..b745ca9 --- /dev/null +++ b/src/active-principle/active-principle.controller.ts @@ -0,0 +1,66 @@ +import { + Controller, + Get, + Query, + UseGuards, + UseInterceptors, + HttpStatus, +} from '@nestjs/common'; +import { ActivePrincipleService } from './active-principle.service'; +import { RolesGuard } from 'src/auth/roles.guard'; +import { AuthGuard } from 'src/auth/auth.guard'; +import { Roles } from 'src/auth/roles.decorador'; +import { UserRole } from 'src/user/entities/user.entity'; +import { PaginationInterceptor } from 'src/utils/pagination.interceptor'; +import { PaginationQueryDTO } from 'src/utils/dto/pagination.dto'; +import { Pagination } from 'src/utils/pagination.decorator'; +import { + ApiOperation, + ApiQuery, + ApiResponse, + getSchemaPath, +} from '@nestjs/swagger'; +import { ResponseActivePrincipleDTO } from './dto/active-principle.dto'; +import { PaginationDTO } from 'src/utils/dto/pagination.dto'; + +@Controller('active-principle') +export class ActivePrincipleController { + constructor( + private readonly activePrincipleService: ActivePrincipleService, + ) {} + + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Get() + @UseInterceptors(PaginationInterceptor) + @ApiOperation({ summary: 'List active principles' }) + @ApiQuery({ name: 'q', required: false, description: 'Search term' }) + @ApiQuery({ name: 'page', required: false, type: Number, example: 1 }) + @ApiQuery({ name: 'limit', required: false, type: Number, example: 10 }) + @ApiResponse({ + description: 'Successful retrieval', + status: HttpStatus.OK, + schema: { + allOf: [ + { $ref: getSchemaPath(PaginationDTO) }, + { + properties: { + results: { + type: 'array', + items: { $ref: getSchemaPath(ResponseActivePrincipleDTO) }, + }, + }, + }, + ], + }, + }) + async findAll( + @Pagination() pagination: PaginationQueryDTO, + @Query('q') q?: string, + ): Promise<{ data: ResponseActivePrincipleDTO[]; total: number }> { + const { page, limit } = pagination; + const data = await this.activePrincipleService.findAll(page, limit, q); + const total = await this.activePrincipleService.countActivePrinciples(q); + return { data, total }; + } +} diff --git a/src/active-principle/active-principle.module.ts b/src/active-principle/active-principle.module.ts new file mode 100644 index 0000000..53cd5d1 --- /dev/null +++ b/src/active-principle/active-principle.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ActivePrinciple } from './entities/active-principle.entity'; +import { ActivePrincipleController } from './active-principle.controller'; +import { ActivePrincipleService } from './active-principle.service'; +import { AuthModule } from 'src/auth/auth.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([ActivePrinciple]), AuthModule], + controllers: [ActivePrincipleController], + providers: [ActivePrincipleService], +}) +export class ActivePrincipleModule {} diff --git a/src/active-principle/active-principle.service.ts b/src/active-principle/active-principle.service.ts new file mode 100644 index 0000000..0cc8a28 --- /dev/null +++ b/src/active-principle/active-principle.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, ILike } from 'typeorm'; +import { ActivePrinciple } from './entities/active-principle.entity'; + +@Injectable() +export class ActivePrincipleService { + constructor( + @InjectRepository(ActivePrinciple) + private readonly activePrincipleRepository: Repository, + ) {} + + async findAll( + page: number, + limit: number, + q?: string, + ): Promise { + const where = q ? { name: ILike(`%${q}%`) } : {}; + return await this.activePrincipleRepository.find({ + where, + skip: (page - 1) * limit, + take: limit, + }); + } + + async countActivePrinciples(q?: string): Promise { + const where = q ? { name: ILike(`%${q}%`) } : {}; + return await this.activePrincipleRepository.count({ where }); + } +} diff --git a/src/active-principle/dto/active-principle.dto.ts b/src/active-principle/dto/active-principle.dto.ts new file mode 100644 index 0000000..621d546 --- /dev/null +++ b/src/active-principle/dto/active-principle.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsString } from 'class-validator'; + +export class ResponseActivePrincipleDTO { + @Expose() + @ApiProperty({ example: 'Acetaminophen' }) + @IsString() + name: string; +} diff --git a/src/active-principle/entities/active-principle.entity.ts b/src/active-principle/entities/active-principle.entity.ts new file mode 100644 index 0000000..ba80495 --- /dev/null +++ b/src/active-principle/entities/active-principle.entity.ts @@ -0,0 +1,8 @@ +import { BaseModel } from 'src/utils/entity'; +import { Column, Entity } from 'typeorm'; + +@Entity() +export class ActivePrinciple extends BaseModel { + @Column({ type: 'varchar', length: 255 }) + name: string; +} diff --git a/src/active-principle/migrations/1747107651171-add-active-principle-migration.ts b/src/active-principle/migrations/1747107651171-add-active-principle-migration.ts new file mode 100644 index 0000000..41363df --- /dev/null +++ b/src/active-principle/migrations/1747107651171-add-active-principle-migration.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddActivePrincipleMigration1747107651171 + implements MigrationInterface +{ + name = 'AddActivePrincipleMigration1747107651171'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "active_principle" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, "name" character varying(255) NOT NULL, CONSTRAINT "PK_872c6d07a950a9ec64da5714eb6" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "active_principle"`); + } +} diff --git a/src/app.module.ts b/src/app.module.ts index a23fa01..66a8fb4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,6 +22,7 @@ import { CartModule } from './cart/cart.module'; import { RecommendationModule } from './recommendation/recommendation.module'; import { ReportsModule } from './reports/reports.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; +import { ActivePrincipleModule } from './active-principle/active-principle.module'; @Module({ imports: [ @@ -67,6 +68,7 @@ import { EventEmitterModule } from '@nestjs/event-emitter'; CartModule, RecommendationModule, ReportsModule, + ActivePrincipleModule, ], controllers: [AppController], providers: [AppService], From 2677e8348925622e7daeb62d2008614d9c1847ea Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Tue, 13 May 2025 00:32:42 -0400 Subject: [PATCH 22/75] Revert "add price" This reverts commit d1a60be3a49dee94c0c73dfa26790a096204b7a2. --- src/order/entities/order.entity.ts | 3 --- ...01623045-add-price-order-detail-migration.ts | 17 ----------------- 2 files changed, 20 deletions(-) delete mode 100644 src/order/migrations/1747101623045-add-price-order-detail-migration.ts diff --git a/src/order/entities/order.entity.ts b/src/order/entities/order.entity.ts index d46698e..47a05b0 100644 --- a/src/order/entities/order.entity.ts +++ b/src/order/entities/order.entity.ts @@ -83,9 +83,6 @@ export class OrderDetail extends UUIDModel { @Column({ type: 'int', name: 'quantity' }) quantity: number; - @Column({ type: 'int', name: 'price' }) - price: number; - @Column({ type: 'int', name: 'subtotal' }) subtotal: number; diff --git a/src/order/migrations/1747101623045-add-price-order-detail-migration.ts b/src/order/migrations/1747101623045-add-price-order-detail-migration.ts deleted file mode 100644 index 13df57d..0000000 --- a/src/order/migrations/1747101623045-add-price-order-detail-migration.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddPriceOrderDetailMigration1747101623045 - implements MigrationInterface -{ - name = 'AddPriceOrderDetailMigration1747101623045'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "order_detail" ADD "price" integer NOT NULL`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "order_detail" DROP COLUMN "price"`); - } -} From 4578ec27e1115125e163fbe8e458451d29910fb1 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Tue, 13 May 2025 15:54:51 -0400 Subject: [PATCH 23/75] add flags to user entity --- src/user/entities/user.entity.ts | 6 +++++ ...47165866071-add-flags-to-user-migration.ts | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/user/migrations/1747165866071-add-flags-to-user-migration.ts diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index 86cadb6..d9fdf18 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -96,4 +96,10 @@ export class User extends BaseModel { @Column({ type: 'character varying', name: 'ws_id', nullable: true }) wsId: string | undefined; + + @Column({ type: 'boolean', default: false, name: 'is_mobile_customer' }) + isMobileCustomer: boolean; + + @Column({ type: 'boolean', default: true, name: 'is_generic_password' }) + isGenericPassword: boolean; } diff --git a/src/user/migrations/1747165866071-add-flags-to-user-migration.ts b/src/user/migrations/1747165866071-add-flags-to-user-migration.ts new file mode 100644 index 0000000..c93d41a --- /dev/null +++ b/src/user/migrations/1747165866071-add-flags-to-user-migration.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddFlagsToUserMigration1747165866071 + implements MigrationInterface +{ + name = 'AddFlagsToUserMigration1747165866071'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ADD "is_mobile_customer" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "is_generic_password" boolean NOT NULL DEFAULT true`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" DROP COLUMN "is_generic_password"`, + ); + await queryRunner.query( + `ALTER TABLE "user" DROP COLUMN "is_mobile_customer"`, + ); + } +} From 254e44639440ec9519697f0b55be90a8d4eed73f Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Tue, 13 May 2025 21:54:14 -0400 Subject: [PATCH 24/75] price added and modified subtotal --- src/order/entities/order.entity.ts | 3 +++ ...72177914-add-price-order-detail-migration.ts | 17 +++++++++++++++++ src/order/order.service.ts | 10 +++++----- 3 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 src/order/migrations/1747172177914-add-price-order-detail-migration.ts diff --git a/src/order/entities/order.entity.ts b/src/order/entities/order.entity.ts index 47a05b0..d46698e 100644 --- a/src/order/entities/order.entity.ts +++ b/src/order/entities/order.entity.ts @@ -83,6 +83,9 @@ export class OrderDetail extends UUIDModel { @Column({ type: 'int', name: 'quantity' }) quantity: number; + @Column({ type: 'int', name: 'price' }) + price: number; + @Column({ type: 'int', name: 'subtotal' }) subtotal: number; diff --git a/src/order/migrations/1747172177914-add-price-order-detail-migration.ts b/src/order/migrations/1747172177914-add-price-order-detail-migration.ts new file mode 100644 index 0000000..50128c0 --- /dev/null +++ b/src/order/migrations/1747172177914-add-price-order-detail-migration.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPriceOrderDetailMigration1747172177914 + implements MigrationInterface +{ + name = 'AddPriceOrderDetailMigration1747172177914'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "order_detail" ADD "price" integer NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "order_detail" DROP COLUMN "price"`); + } +} diff --git a/src/order/order.service.ts b/src/order/order.service.ts index da50598..e7794f5 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -103,6 +103,7 @@ export class OrderService { ); } } + let totalPrice = productsWithQuantity.reduce((acc, product) => { const price = product.promo ? product.price - (product.price * product.promo.discount) / 100 @@ -110,6 +111,8 @@ export class OrderService { return acc + Math.round(price) * product.quantity; }, 0); + console.log('Total pagado:', totalPrice); + if (productsWithQuantity.length == 0) { throw new BadRequestException('No products found'); } @@ -132,11 +135,8 @@ export class OrderService { order, productPresentation: product, quantity: product.quantity, - subtotal: product.promo - ? Math.round( - product.price - (product.price * product.promo.discount) / 100, - ) * product.quantity - : product.price * product.quantity, + price: product.price, + subtotal: product.price * product.quantity, }); }); await this.orderDetailRepository.save(orderDetails); From 505b0a9031b90bd0343c03330ba5ce2373e04b94 Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Tue, 13 May 2025 22:01:59 -0400 Subject: [PATCH 25/75] fix --- src/order/order.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/order/order.service.ts b/src/order/order.service.ts index e7794f5..7132143 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -111,8 +111,6 @@ export class OrderService { return acc + Math.round(price) * product.quantity; }, 0); - console.log('Total pagado:', totalPrice); - if (productsWithQuantity.length == 0) { throw new BadRequestException('No products found'); } From 1a16db43990698ed0735089c3e5726e85f9c1245 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Wed, 14 May 2025 11:14:59 -0400 Subject: [PATCH 26/75] Remove order by product priority --- package-lock.json | 4 ++-- src/products/products.service.ts | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1fc02f..1250bcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "api", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "api", - "version": "0.4.0", + "version": "0.5.0", "license": "UNLICENSED", "dependencies": { "@nestjs/cli": "^11.0.5", diff --git a/src/products/products.service.ts b/src/products/products.service.ts index 2281a8c..b797ae2 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -119,9 +119,6 @@ export class ProductsService { }, order: { createdAt: 'DESC', - product: { - priority: 'ASC', - }, }, where, skip: (page - 1) * limit, From 1ba35948593ed02df948db4619fb483b48d3c7f4 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Wed, 14 May 2025 14:57:21 -0400 Subject: [PATCH 27/75] Fix error handling and set wsId blank --- src/order/order.gateway.ts | 55 +++++++++++++++++++++++++++----------- src/user/user.service.ts | 3 +-- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/order/order.gateway.ts b/src/order/order.gateway.ts index dc64c88..72e0c38 100644 --- a/src/order/order.gateway.ts +++ b/src/order/order.gateway.ts @@ -61,15 +61,28 @@ export class OrderGateway implements OnGatewayConnection, OnGatewayDisconnect { @ConnectedSocket() client: Socket, @MessageBody() data: UpdateOrderStatusWsDTO, ) { - this.orderService.findOneWithUser(data.id).then((order) => { - this.orderService.getUserByOrderId(order.id).then((user) => { - if (user.wsId) { - client - .to(user.wsId) - .emit('orderUpdated', { orderId: order.id, status: data.status }); + this.orderService + .findOneWithUser(data.id) + .then((order) => { + this.orderService + .getUserByOrderId(order.id) + .then((user) => { + if (user.wsId) { + client.to(user.wsId).emit('orderUpdated', { + orderId: order.id, + status: data.status, + }); + } + }) + .catch((error) => { + client.to(client.id).emit('error', error); + }); + }) + .catch((error) => { + { + client.to(client.id).emit('error', error); } }); - }); } @UseFilters(new WebsocketExceptionsFilter()) @@ -85,16 +98,26 @@ export class OrderGateway implements OnGatewayConnection, OnGatewayDisconnect { @ConnectedSocket() client: Socket, @MessageBody() data: UpdateDeliveryWsDTO, ) { - this.orderService.getDelivery(data.id).then((delivery) => { - this.orderService.getUserByOrderId(delivery.order.id).then((user) => { - if (user.wsId) { - client.to(user.wsId).emit('deliveryUpdated', { - orderDeliveryId: delivery.id, - status: data.deliveryStatus, - employeeId: delivery.employee.id, + this.orderService + .getDelivery(data.id) + .then((delivery) => { + this.orderService + .getUserByOrderId(delivery.order.id) + .then((user) => { + if (user.wsId) { + client.to(user.wsId).emit('deliveryUpdated', { + orderDeliveryId: delivery.id, + status: data.deliveryStatus, + employeeId: delivery.employee.id, + }); + } + }) + .catch((error) => { + client.to(client.id).emit('error', error); }); - } + }) + .catch((error) => { + client.to(client.id).emit('error', error); }); - }); } } diff --git a/src/user/user.service.ts b/src/user/user.service.ts index d43d58c..5b342c4 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -384,7 +384,6 @@ export class UserService { if (!user) { return; } - user.wsId = undefined; - await this.userRepository.save(user); + await this.userRepository.update(user.id, { wsId: '' }); } } From d0f2e215be1ba0f48acab5be0d19e8b4f8f1e5af Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Wed, 14 May 2025 15:01:56 -0400 Subject: [PATCH 28/75] fix --- src/order/order.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/order/order.service.ts b/src/order/order.service.ts index da50598..5b5ff21 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -550,7 +550,7 @@ export class OrderService { .addSelect('o.type', 'type') .addSelect('SUM(d.quantity)', 'quantity') .addSelect('SUM(d.subtotal)', 'subtotal') - .addSelect('SUM(d.quantity * pp.price) - o.totalPrice', 'discount') + .addSelect('SUM(d.quantity * d.price) - o.totalPrice', 'discount') .addSelect('o.totalPrice', 'total') .innerJoin('o.user', 'u') .innerJoin('o.details', 'd') From e6f5cf9c18ba9a47cc718d8363e86937914b22ff Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Wed, 14 May 2025 15:26:13 -0400 Subject: [PATCH 29/75] imports AuthModule, UserModule and BranchModule --- src/auth/auth.module.ts | 2 ++ src/branch/branch.module.ts | 5 +++-- src/user/dto/user.dto.ts | 10 ++++++++++ src/user/user.module.ts | 2 ++ src/user/user.service.ts | 15 +++++++++++++++ 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index f24662b..f7e9789 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -7,10 +7,12 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { UserService } from 'src/user/user.service'; import { EmailModule } from 'src/email/email.module'; import { AuthGuard } from './auth.guard'; +import { BranchModule } from 'src/branch/branch.module'; @Module({ imports: [ forwardRef(() => UserModule), + forwardRef(() => BranchModule), ConfigModule, JwtModule.registerAsync({ useFactory: (configService: ConfigService) => ({ diff --git a/src/branch/branch.module.ts b/src/branch/branch.module.ts index 09693be..46e7bd2 100644 --- a/src/branch/branch.module.ts +++ b/src/branch/branch.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { BranchService } from './branch.service'; import { BranchController } from './branch.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -14,9 +14,10 @@ import { State } from 'src/state/entities/state.entity'; @Module({ imports: [ TypeOrmModule.forFeature([Branch, City, State, Country]), - AuthModule, + forwardRef(() => AuthModule), ], controllers: [BranchController], providers: [BranchService, CityService, StateService, CountryService], + exports: [BranchService], }) export class BranchModule {} diff --git a/src/user/dto/user.dto.ts b/src/user/dto/user.dto.ts index 912d564..a9eab9d 100644 --- a/src/user/dto/user.dto.ts +++ b/src/user/dto/user.dto.ts @@ -16,6 +16,7 @@ import { MaxLength, MinLength, IsString, + IsUUID, } from 'class-validator'; import { UserGender } from '../entities/profile.entity'; import { IsOlderThan } from 'src/utils/is-older-than-validator'; @@ -98,6 +99,15 @@ export class UserAdminDTO extends BaseUserDTO { @Expose() role: UserRole; + @ApiProperty({ + description: 'branchId of the user (branch_admin or delivery)', + required: false, + }) + @IsOptional() + @IsUUID() + @Expose() + branchId?: string; + // Data use if it is a delivery @ApiProperty({ description: 'Motorcycle brand', required: false }) diff --git a/src/user/user.module.ts b/src/user/user.module.ts index 8e858e4..ebfd96f 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -9,6 +9,7 @@ import { AuthModule } from 'src/auth/auth.module'; import { Branch } from 'src/branch/entities/branch.entity'; import { UserAddress } from './entities/user-address.entity'; import { UserMoto } from './entities/user-moto.entity'; +import { BranchModule } from 'src/branch/branch.module'; @Module({ imports: [ @@ -21,6 +22,7 @@ import { UserMoto } from './entities/user-moto.entity'; UserMoto, ]), forwardRef(() => AuthModule), + forwardRef(() => BranchModule), ], providers: [UserService], exports: [UserService, TypeOrmModule], diff --git a/src/user/user.service.ts b/src/user/user.service.ts index d43d58c..6976033 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -16,6 +16,7 @@ import { CreateUserAddressDTO } from './dto/user-address.dto'; import { ConfigService } from '@nestjs/config'; import { UserMoto } from './entities/user-moto.entity'; import { UpdateUserMotoDTO } from './dto/user-moto.dto'; +import { BranchService } from 'src/branch/branch.service'; @Injectable() export class UserService { @@ -31,6 +32,8 @@ export class UserService { @InjectRepository(UserMoto) private UserMotoRepository: Repository, private configService: ConfigService, + + private readonly branchService: BranchService, ) {} async userExists(options: Partial): Promise { @@ -126,6 +129,18 @@ export class UserService { const hashedPassword = await bcrypt.hash(password, 10); const newUser = this.userRepository.create(user); newUser.password = hashedPassword; + if ( + user.role === UserRole.BRANCH_ADMIN || + user.role === UserRole.DELIVERY + ) { + if (!user.branchId) { + throw new BadRequestException('branchId is required for this role'); + } + + const branch = await this.branchService.findOne(user.branchId); + + newUser.branch = branch; + } const userCreated = await this.userRepository.save(newUser); const profile = new Profile(); profile.user = userCreated; From 3dd7df55dcc98a9c73ecad5e858c31e582e3fb84 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Wed, 14 May 2025 16:05:40 -0400 Subject: [PATCH 30/75] add send email to the created account --- src/user/user.module.ts | 2 ++ src/user/user.service.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/user/user.module.ts b/src/user/user.module.ts index ebfd96f..cc33946 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -10,6 +10,7 @@ import { Branch } from 'src/branch/entities/branch.entity'; import { UserAddress } from './entities/user-address.entity'; import { UserMoto } from './entities/user-moto.entity'; import { BranchModule } from 'src/branch/branch.module'; +import { EmailModule } from 'src/email/email.module'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { BranchModule } from 'src/branch/branch.module'; ]), forwardRef(() => AuthModule), forwardRef(() => BranchModule), + EmailModule, ], providers: [UserService], exports: [UserService, TypeOrmModule], diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 6976033..4e2d8b5 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -17,6 +17,7 @@ import { ConfigService } from '@nestjs/config'; import { UserMoto } from './entities/user-moto.entity'; import { UpdateUserMotoDTO } from './dto/user-moto.dto'; import { BranchService } from 'src/branch/branch.service'; +import { EmailService } from 'src/email/email.service'; @Injectable() export class UserService { @@ -34,6 +35,7 @@ export class UserService { private configService: ConfigService, private readonly branchService: BranchService, + private emailService: EmailService, ) {} async userExists(options: Partial): Promise { @@ -142,6 +144,14 @@ export class UserService { newUser.branch = branch; } const userCreated = await this.userRepository.save(newUser); + + await this.emailService.sendEmail({ + recipients: [{ email: user.email, name: user.firstName }], + subject: 'Welcome to Pharmatech', + html: `

Hi, your account has been created. Your password is: ${password}

`, + text: `Hi, your account has been created. Your password is: ${password}`, + }); + const profile = new Profile(); profile.user = userCreated; if (user.gender) { From 64065a098522e54e9540839a1dee25c191742116 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Wed, 14 May 2025 17:58:28 -0400 Subject: [PATCH 31/75] fix column isGenericPassword default false --- src/user/entities/user.entity.ts | 2 +- ...tion.ts => 1747259587010-add-flags-to-user-migration.ts} | 6 +++--- src/user/user.service.ts | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) rename src/user/migrations/{1747165866071-add-flags-to-user-migration.ts => 1747259587010-add-flags-to-user-migration.ts} (84%) diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index d9fdf18..da83a58 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -100,6 +100,6 @@ export class User extends BaseModel { @Column({ type: 'boolean', default: false, name: 'is_mobile_customer' }) isMobileCustomer: boolean; - @Column({ type: 'boolean', default: true, name: 'is_generic_password' }) + @Column({ type: 'boolean', default: false, name: 'is_generic_password' }) isGenericPassword: boolean; } diff --git a/src/user/migrations/1747165866071-add-flags-to-user-migration.ts b/src/user/migrations/1747259587010-add-flags-to-user-migration.ts similarity index 84% rename from src/user/migrations/1747165866071-add-flags-to-user-migration.ts rename to src/user/migrations/1747259587010-add-flags-to-user-migration.ts index c93d41a..b9387c9 100644 --- a/src/user/migrations/1747165866071-add-flags-to-user-migration.ts +++ b/src/user/migrations/1747259587010-add-flags-to-user-migration.ts @@ -1,16 +1,16 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddFlagsToUserMigration1747165866071 +export class AddFlagsToUserMigration1747259587010 implements MigrationInterface { - name = 'AddFlagsToUserMigration1747165866071'; + name = 'AddFlagsToUserMigration1747259587010'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( `ALTER TABLE "user" ADD "is_mobile_customer" boolean NOT NULL DEFAULT false`, ); await queryRunner.query( - `ALTER TABLE "user" ADD "is_generic_password" boolean NOT NULL DEFAULT true`, + `ALTER TABLE "user" ADD "is_generic_password" boolean NOT NULL DEFAULT false`, ); } diff --git a/src/user/user.service.ts b/src/user/user.service.ts index b5ab4b0..a2849ed 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -131,6 +131,7 @@ export class UserService { const hashedPassword = await bcrypt.hash(password, 10); const newUser = this.userRepository.create(user); newUser.password = hashedPassword; + newUser.isGenericPassword = true; if ( user.role === UserRole.BRANCH_ADMIN || user.role === UserRole.DELIVERY From 5ebca3363be81ab1f882e183138ebe1f9eac856c Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Wed, 14 May 2025 18:49:55 -0400 Subject: [PATCH 32/75] Set default value to 0 --- src/order/entities/order.entity.ts | 2 +- .../1747172177914-add-price-order-detail-migration.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/order/entities/order.entity.ts b/src/order/entities/order.entity.ts index d46698e..d002a07 100644 --- a/src/order/entities/order.entity.ts +++ b/src/order/entities/order.entity.ts @@ -83,7 +83,7 @@ export class OrderDetail extends UUIDModel { @Column({ type: 'int', name: 'quantity' }) quantity: number; - @Column({ type: 'int', name: 'price' }) + @Column({ type: 'int', name: 'price', default: 0 }) price: number; @Column({ type: 'int', name: 'subtotal' }) diff --git a/src/order/migrations/1747172177914-add-price-order-detail-migration.ts b/src/order/migrations/1747172177914-add-price-order-detail-migration.ts index 50128c0..404b0e6 100644 --- a/src/order/migrations/1747172177914-add-price-order-detail-migration.ts +++ b/src/order/migrations/1747172177914-add-price-order-detail-migration.ts @@ -7,7 +7,7 @@ export class AddPriceOrderDetailMigration1747172177914 public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `ALTER TABLE "order_detail" ADD "price" integer NOT NULL`, + `ALTER TABLE "order_detail" ADD "price" integer DEFAULT 0 NOT NULL`, ); } From 8a26c90ef882eb1c8a4736c3255754f638e6b52c Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Wed, 14 May 2025 19:08:23 -0400 Subject: [PATCH 33/75] add isGenericPassword to false when changing password --- src/auth/auth.service.ts | 5 ++++- src/user/dto/user.dto.ts | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 361f257..54ec600 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -92,7 +92,10 @@ export class AuthService { async updatePassword(user: User, newPasswod: string): Promise { const password = await this.encryptPassword(newPasswod); - await this.userService.update(user, { password }); + await this.userService.update(user, { + password, + isGenericPassword: false, + }); return true; } diff --git a/src/user/dto/user.dto.ts b/src/user/dto/user.dto.ts index a9eab9d..fbf5c83 100644 --- a/src/user/dto/user.dto.ts +++ b/src/user/dto/user.dto.ts @@ -17,6 +17,7 @@ import { MinLength, IsString, IsUUID, + IsBoolean, } from 'class-validator'; import { UserGender } from '../entities/profile.entity'; import { IsOlderThan } from 'src/utils/is-older-than-validator'; @@ -89,6 +90,14 @@ export class BaseUserDTO { @IsOptional() @IsEnum(UserGender) gender?: UserGender; + + @IsOptional() + @IsBoolean() + @Expose() + @ApiProperty({ + description: 'Indicates whether the user has a generic password', + }) + isGenericPassword?: boolean; } export class UserDTO extends IntersectionType(BaseUserDTO, PasswordDTO) {} From 76a387c52a226b55b081ca82b0e1847da665b45c Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 15 May 2025 22:47:13 -0400 Subject: [PATCH 34/75] Create endpoint for bulk update user role and is validated column --- src/user/dto/user.dto.ts | 21 +++++++++++++++++++++ src/user/user.controller.ts | 14 ++++++++++++++ src/user/user.service.ts | 25 +++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/user/dto/user.dto.ts b/src/user/dto/user.dto.ts index fbf5c83..f7cf93a 100644 --- a/src/user/dto/user.dto.ts +++ b/src/user/dto/user.dto.ts @@ -18,6 +18,7 @@ import { IsString, IsUUID, IsBoolean, + ArrayNotEmpty, } from 'class-validator'; import { UserGender } from '../entities/profile.entity'; import { IsOlderThan } from 'src/utils/is-older-than-validator'; @@ -102,6 +103,26 @@ export class BaseUserDTO { export class UserDTO extends IntersectionType(BaseUserDTO, PasswordDTO) {} +export class UserBulkUpdateDTO { + @IsOptional() + @ApiProperty({ description: 'If the user has validated the email' }) + isValidated: boolean; + + @IsOptional() + @IsEnum(UserRole) + @ApiProperty({ description: 'Role of the user', enum: UserRole }) + role: UserRole; +} + +export class UserListUpdateDTO { + @ArrayNotEmpty() + @IsUUID(undefined, { each: true }) + users: string[]; + + @Type(() => UserBulkUpdateDTO) + data: UserBulkUpdateDTO; +} + export class UserAdminDTO extends BaseUserDTO { @ApiProperty({ description: 'the role of the user' }) @IsNotEmpty() diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 8d7ee8b..10f67f4 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -36,6 +36,7 @@ import { UserAdminDTO, UpdateUserDTO, UserMotoDTO, + UserListUpdateDTO, } from './dto/user.dto'; import { PaginationDTO, UserQueryDTO } from 'src/utils/dto/pagination.dto'; import { plainToInstance } from 'class-transformer'; @@ -75,6 +76,19 @@ export class UserController { await this.userService.validateEmail(userOtp); } + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Post('bulk') + @ApiBearerAuth() + @ApiOperation({ summary: 'Bulk update users' }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Users updated successfully', + }) + async bulkUpdate(@Body() updateUserDto: UserListUpdateDTO): Promise { + await this.userService.bulkUpdate(updateUserDto.users, updateUserDto.data); + } + @HttpCode(HttpStatus.OK) @Get(':userId') @UseGuards(AuthGuard, UserOrAdminGuard) diff --git a/src/user/user.service.ts b/src/user/user.service.ts index a2849ed..fc2b362 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -5,8 +5,13 @@ import { BadRequestException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { UserAdminDTO, UserDTO, UpdateUserDTO } from './dto/user.dto'; +import { In, Repository } from 'typeorm'; +import { + UserAdminDTO, + UserDTO, + UpdateUserDTO, + UserBulkUpdateDTO, +} from './dto/user.dto'; import { User, UserRole } from './entities/user.entity'; import { UserOTP } from './entities/user-otp.entity'; import { Profile } from './entities/profile.entity'; @@ -412,4 +417,20 @@ export class UserService { } await this.userRepository.update(user.id, { wsId: '' }); } + + async bulkUpdate( + userIds: string[], + userDto: UserBulkUpdateDTO, + ): Promise { + const users = await this.userRepository.findBy({ id: In(userIds) }); + if (!users.length) { + throw new NotFoundException('No users found'); + } + + const updatedUsers = users.map((user) => { + return { ...user, ...userDto }; + }); + + return await this.userRepository.save(updatedUsers); + } } From ce3bc37f020ab9ae244ce78c94409b5186aa0e0e Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 15 May 2025 22:58:10 -0400 Subject: [PATCH 35/75] Create bulk update for order status --- src/order/controllers/order.controller.ts | 17 +++++++++++++++++ src/order/dto/order.ts | 11 +++++++++++ src/order/order.service.ts | 18 +++++++++++++++++- src/user/user.controller.ts | 2 +- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/order/controllers/order.controller.ts b/src/order/controllers/order.controller.ts index e6094b8..3aefbd3 100644 --- a/src/order/controllers/order.controller.ts +++ b/src/order/controllers/order.controller.ts @@ -17,6 +17,7 @@ import { import { OrderService } from '../order.service'; import { CreateOrderDTO, + OrderListUpdateDTO, OrderQueryDTO, ResponseOrderDetailedDTO, ResponseOrderDTO, @@ -162,6 +163,22 @@ export class OrderController { }; } + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Patch('bulk') + @ApiBearerAuth() + @ApiOperation({ summary: 'Bulk update orders' }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Orders updated successfully', + }) + async bulkUpdate(@Body() updateOrderDto: OrderListUpdateDTO): Promise { + await this.orderService.bulkUpdate( + updateOrderDto.orders, + updateOrderDto.status, + ); + } + @Get(':id') @UseGuards(AuthGuard) @ApiBearerAuth() diff --git a/src/order/dto/order.ts b/src/order/dto/order.ts index 1096e04..eb5eb5e 100644 --- a/src/order/dto/order.ts +++ b/src/order/dto/order.ts @@ -3,6 +3,7 @@ import { IsArray, IsEnum, IsInt, + IsNotEmpty, IsOptional, IsPositive, IsString, @@ -213,6 +214,16 @@ export class UpdateOrderStatusWsDTO { status: OrderStatus; } +export class OrderListUpdateDTO { + @ArrayNotEmpty() + @IsUUID(undefined, { each: true }) + orders: string[]; + + @IsNotEmpty() + @IsEnum(OrderStatus) + status: OrderStatus; +} + export class SalesReportDTO { @Expose() @ApiProperty({ description: 'ID of the order' }) diff --git a/src/order/order.service.ts b/src/order/order.service.ts index f15e652..bc41728 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -7,7 +7,7 @@ import { CreateOrderDTO, SalesReportDTO } from './dto/order'; import { User, UserRole } from 'src/user/entities/user.entity'; import { ProductPresentationService } from 'src/products/services/product-presentation.service'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { Order, OrderDetail, @@ -276,6 +276,22 @@ export class OrderService { return await this.orderRepository.save(order); } + async bulkUpdate(ordersIds: string[], status: OrderStatus) { + const orders = await this.orderRepository.findBy({ + id: In(ordersIds), + }); + if (orders.length === 0) { + throw new NotFoundException('No orders found'); + } + const updatedOrders = orders.map((order) => { + if (order.status !== OrderStatus.COMPLETED) { + order.status = status; + return order; + } else return order; + }); + return await this.orderRepository.save(updatedOrders); + } + async findAllOD( user: User, page: number, diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 10f67f4..6c1ac9f 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -78,7 +78,7 @@ export class UserController { @UseGuards(AuthGuard, RolesGuard) @Roles(UserRole.ADMIN) - @Post('bulk') + @Patch('bulk') @ApiBearerAuth() @ApiOperation({ summary: 'Bulk update users' }) @ApiResponse({ From ce705c9759d43fc093cb72e2eeda1ea788c870c3 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 15 May 2025 23:05:52 -0400 Subject: [PATCH 36/75] Create bulk update for product presentation visibility --- .../product-presentation.controller.ts | 23 ++++++++++++++++++- src/products/dto/product-presentation.dto.ts | 13 +++++++++++ .../services/product-presentation.service.ts | 16 +++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/products/controllers/product-presentation.controller.ts b/src/products/controllers/product-presentation.controller.ts index e41d393..1c3470a 100644 --- a/src/products/controllers/product-presentation.controller.ts +++ b/src/products/controllers/product-presentation.controller.ts @@ -13,7 +13,10 @@ import { UseGuards, } from '@nestjs/common'; import { ProductPresentationService } from '../services/product-presentation.service'; -import { CreateProductPresentationDTO } from '../dto/product-presentation.dto'; +import { + CreateProductPresentationDTO, + ProductPresentationListUpdateDTO, +} from '../dto/product-presentation.dto'; import { PresentationService } from '../services/presentation.service'; import { AuthGuard } from 'src/auth/auth.guard'; import { RolesGuard } from 'src/auth/roles.guard'; @@ -74,6 +77,24 @@ export class ProductPresentationController { ); } + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Patch('bulk') + @ApiBearerAuth() + @ApiOperation({ summary: 'Bulk update orders' }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Orders updated successfully', + }) + async bulkUpdate( + @Body() updateProductPresentationDto: ProductPresentationListUpdateDTO, + ): Promise { + await this.productPresentationService.bulkUpdate( + updateProductPresentationDto.ids, + updateProductPresentationDto.isVisible, + ); + } + @Get(':presentationId') @ApiOperation({ summary: 'Get product presentation by product and presentation ID', diff --git a/src/products/dto/product-presentation.dto.ts b/src/products/dto/product-presentation.dto.ts index 43b01fb..0d54193 100644 --- a/src/products/dto/product-presentation.dto.ts +++ b/src/products/dto/product-presentation.dto.ts @@ -93,3 +93,16 @@ export class ResponseOrderProductPresentationDetailDTO extends IntersectionType( @ApiProperty({ type: ResponsePromoDTO }) promo: ResponsePromoDTO; } + +export class ProductPresentationListUpdateDTO { + @ApiProperty({ description: 'The IDs of the product presentation' }) + @IsUUID(undefined, { each: true }) + ids: string[]; + + @ApiProperty({ + description: 'Indicates if the product presentation is visible', + default: true, + }) + @IsBoolean() + isVisible: boolean; +} diff --git a/src/products/services/product-presentation.service.ts b/src/products/services/product-presentation.service.ts index 1921bee..4af0ba7 100644 --- a/src/products/services/product-presentation.service.ts +++ b/src/products/services/product-presentation.service.ts @@ -115,6 +115,22 @@ export class ProductPresentationService { return await this.repository.save(updatedProductPresentation); } + async bulkUpdate(productPresentationIds: string[], isVisible: boolean) { + const productPresentations = await this.repository.findBy({ + id: In(productPresentationIds), + }); + if (productPresentations.length === 0) { + throw new NotFoundException('No product presentations found'); + } + const updatedProductPresentations = productPresentations.map( + (productPresentation) => { + productPresentation.isVisible = isVisible; + return productPresentation; + }, + ); + return await this.repository.save(updatedProductPresentations); + } + async remove(productId: string, presentationId: string): Promise { const productPresentation = await this.findOneProductPresentation( productId, From f41ce434b3c5a387caef74a702df4cd79c97acfc Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Fri, 16 May 2025 09:05:45 -0400 Subject: [PATCH 37/75] Create bulk delete and bulk update for promos --- src/discount/controllers/promo.controller.ts | 31 ++++++++++++++++++++ src/discount/dto/promo.dto.ts | 23 +++++++++++++++ src/discount/services/promo.service.ts | 27 ++++++++++++++++- 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/discount/controllers/promo.controller.ts b/src/discount/controllers/promo.controller.ts index cd45416..6d7627d 100644 --- a/src/discount/controllers/promo.controller.ts +++ b/src/discount/controllers/promo.controller.ts @@ -29,6 +29,8 @@ import { UpdatePromoDTO, ResponsePromoDTO, PromoQueryDTO, + PromoListDeleteDTO, + PromoListUpdateDTO, } from '../dto/promo.dto'; import { PromoService } from '../services/promo.service'; import { UserRole } from 'src/user/entities/user.entity'; @@ -115,6 +117,35 @@ export class PromoController { return { data, total }; } + @HttpCode(HttpStatus.NO_CONTENT) + @Delete('bulk') + @ApiOperation({ summary: 'Bulk delete promos' }) + @ApiResponse({ + description: 'Successful bulk deletion of promos', + status: HttpStatus.NO_CONTENT, + }) + async bulkDelete( + @Body() promoListDeleteDTO: PromoListDeleteDTO, + ): Promise { + await this.promoService.bulkDelete(promoListDeleteDTO.ids); + } + + @HttpCode(HttpStatus.NO_CONTENT) + @Patch('bulk') + @ApiOperation({ summary: 'Bulk delete promos' }) + @ApiResponse({ + description: 'Successful bulk deletion of promos', + status: HttpStatus.NO_CONTENT, + }) + async bulkUpdate( + @Body() promoListUpdateDTO: PromoListUpdateDTO, + ): Promise { + await this.promoService.bulkUpdate( + promoListUpdateDTO.ids, + promoListUpdateDTO.expiredAt, + ); + } + @Get(':id') @ApiOperation({ summary: 'Get promo by ID' }) @ApiResponse({ diff --git a/src/discount/dto/promo.dto.ts b/src/discount/dto/promo.dto.ts index f32c012..6e1bdbc 100644 --- a/src/discount/dto/promo.dto.ts +++ b/src/discount/dto/promo.dto.ts @@ -6,6 +6,7 @@ import { IsString, IsDateString, IsOptional, + IsUUID, } from 'class-validator'; import { BaseDTO } from 'src/utils/dto/base.dto'; import { PaginationQueryDTO } from 'src/utils/dto/pagination.dto'; @@ -58,3 +59,25 @@ export class PromoQueryDTO extends PaginationQueryDTO { this.expirationBetween = expirationBetween ? expirationBetween : []; } } + +export class PromoListDeleteDTO { + @IsUUID(undefined, { each: true }) + @ApiProperty({ + description: 'List of promo ids to be deleted', + type: [String], + }) + ids: string[]; +} + +export class PromoListUpdateDTO { + @IsUUID(undefined, { each: true }) + @ApiProperty({ + description: 'List of promo ids to be updated', + type: [String], + }) + ids: string[]; + + @IsNotEmpty() + @ApiProperty({ description: 'The new expiration date of the promos' }) + expiredAt: Date; +} diff --git a/src/discount/services/promo.service.ts b/src/discount/services/promo.service.ts index edbea56..b07b655 100644 --- a/src/discount/services/promo.service.ts +++ b/src/discount/services/promo.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { IsNull, Repository } from 'typeorm'; +import { In, IsNull, Repository } from 'typeorm'; import { PromoDTO, UpdatePromoDTO } from '../dto/promo.dto'; import { Promo } from '../entities/promo.entity'; @@ -69,4 +69,29 @@ export class PromoService { }); return result.affected === 1; } + + async bulkDelete(ids: string[]) { + const promos = await this.promoRepository.findBy({ + id: In(ids), + deletedAt: IsNull(), + }); + if (promos.length === 0) { + throw new NotFoundException(`No promos found with the given IDs`); + } + await this.promoRepository.softDelete({ id: In(ids) }); + } + + async bulkUpdate(ids: string[], expiredAt: Date) { + const promos = await this.promoRepository.findBy({ + id: In(ids), + deletedAt: IsNull(), + }); + if (promos.length === 0) { + throw new NotFoundException(`No promos found with the given IDs`); + } + await this.promoRepository.update( + { id: In(ids) }, + { expiredAt, updatedAt: new Date() }, + ); + } } From 3f99b0b0e6f6a01c0903691ab57aae3f7a76500c Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Fri, 16 May 2025 09:38:16 -0400 Subject: [PATCH 38/75] Create bulk delete and bulk update for coupons --- src/discount/controllers/coupon.controller.ts | 31 ++++++++++++++++ src/discount/controllers/promo.controller.ts | 4 +-- src/discount/dto/coupon.dto.ts | 36 ++++++++++++++++++- src/discount/services/coupon.service.ts | 33 +++++++++++++++-- 4 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/discount/controllers/coupon.controller.ts b/src/discount/controllers/coupon.controller.ts index 039929e..a90051c 100644 --- a/src/discount/controllers/coupon.controller.ts +++ b/src/discount/controllers/coupon.controller.ts @@ -18,6 +18,8 @@ import { UpdateCouponDTO, ResponseCouponDTO, CouponQueryDTO, + CouponListDeleteDTO, + CouponListUpdateDTO, } from '../dto/coupon.dto'; import { AuthGuard } from 'src/auth/auth.guard'; import { RolesGuard } from 'src/auth/roles.guard'; @@ -115,6 +117,35 @@ export class CouponController { return { data, total }; } + @HttpCode(HttpStatus.NO_CONTENT) + @Delete('bulk') + @ApiOperation({ summary: 'Bulk delete coupons' }) + @ApiResponse({ + description: 'Successful bulk deletion of coupons', + status: HttpStatus.NO_CONTENT, + }) + async bulkDelete( + @Body() couponListDeleteDTO: CouponListDeleteDTO, + ): Promise { + await this.couponService.bulkDelete(couponListDeleteDTO.ids); + } + + @HttpCode(HttpStatus.NO_CONTENT) + @Patch('bulk') + @ApiOperation({ summary: 'Bulk update coupons' }) + @ApiResponse({ + description: 'Successful bulk update of coupons', + status: HttpStatus.NO_CONTENT, + }) + async bulkUpdate( + @Body() couponListUpdateDTO: CouponListUpdateDTO, + ): Promise { + await this.couponService.bulkUpdate( + couponListUpdateDTO.ids, + couponListUpdateDTO.data, + ); + } + @Get(':code') @ApiOperation({ summary: 'Get coupon by code' }) @ApiResponse({ diff --git a/src/discount/controllers/promo.controller.ts b/src/discount/controllers/promo.controller.ts index 6d7627d..9cde9a1 100644 --- a/src/discount/controllers/promo.controller.ts +++ b/src/discount/controllers/promo.controller.ts @@ -132,9 +132,9 @@ export class PromoController { @HttpCode(HttpStatus.NO_CONTENT) @Patch('bulk') - @ApiOperation({ summary: 'Bulk delete promos' }) + @ApiOperation({ summary: 'Bulk update promos' }) @ApiResponse({ - description: 'Successful bulk deletion of promos', + description: 'Successful bulk update of promos', status: HttpStatus.NO_CONTENT, }) async bulkUpdate( diff --git a/src/discount/dto/coupon.dto.ts b/src/discount/dto/coupon.dto.ts index 0454d38..e01bd24 100644 --- a/src/discount/dto/coupon.dto.ts +++ b/src/discount/dto/coupon.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, PartialType, IntersectionType } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { IsNotEmpty, IsString, @@ -8,6 +8,7 @@ import { Min, IsDateString, IsOptional, + IsUUID, } from 'class-validator'; import { BaseDTO } from 'src/utils/dto/base.dto'; import { PaginationQueryDTO } from 'src/utils/dto/pagination.dto'; @@ -73,3 +74,36 @@ export class CouponQueryDTO extends PaginationQueryDTO { this.expirationBetween = expirationBetween ? expirationBetween : []; } } + +export class CouponListDeleteDTO { + @IsUUID(undefined, { each: true }) + @ApiProperty({ + description: 'List of coupon ids to be deleted', + type: [String], + }) + ids: string[]; +} + +export class CouponBulkUpdateDTO { + @IsOptional() + @ApiProperty({ description: 'The new expiration date of the coupons' }) + expirationDate: Date; + + @ApiProperty({ description: 'Maximum number of coupon uses' }) + @IsOptional() + @IsInt() + @Min(0) + maxUses: number; +} + +export class CouponListUpdateDTO { + @IsUUID(undefined, { each: true }) + @ApiProperty({ + description: 'List of coupon ids to be updated', + type: [String], + }) + ids: string[]; + + @Type(() => CouponBulkUpdateDTO) + data: CouponBulkUpdateDTO; +} diff --git a/src/discount/services/coupon.service.ts b/src/discount/services/coupon.service.ts index 1b6536b..d75c3be 100644 --- a/src/discount/services/coupon.service.ts +++ b/src/discount/services/coupon.service.ts @@ -1,8 +1,12 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, IsNull, Repository } from 'typeorm'; import { Coupon } from '../entities/coupon.entity'; -import { CouponDTO, UpdateCouponDTO } from '../dto/coupon.dto'; +import { + CouponBulkUpdateDTO, + CouponDTO, + UpdateCouponDTO, +} from '../dto/coupon.dto'; @Injectable() export class CouponService { @@ -76,4 +80,29 @@ export class CouponService { } return true; } + + async bulkDelete(ids: string[]) { + const coupons = await this.couponRepository.findBy({ + id: In(ids), + deletedAt: IsNull(), + }); + if (coupons.length === 0) { + throw new NotFoundException(`No coupons found with the given IDs`); + } + await this.couponRepository.softDelete({ id: In(ids) }); + } + + async bulkUpdate(ids: string[], updateDto: CouponBulkUpdateDTO) { + const coupons = await this.couponRepository.findBy({ + id: In(ids), + deletedAt: IsNull(), + }); + if (coupons.length === 0) { + throw new NotFoundException(`No coupons found with the given IDs`); + } + const couponsToUpdate = coupons.map((coupon) => { + return { ...coupon, ...updateDto }; + }); + await this.couponRepository.save(couponsToUpdate); + } } From b9912be67a33a8493127b5483e0db77618ccd82a Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Fri, 16 May 2025 16:19:53 -0400 Subject: [PATCH 39/75] add paymentConfirmation to orderDetail entity --- src/order/entities/order.entity.ts | 8 ++++++ ...-confirmation-in-order-detail-migration.ts | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/order/migrations/1747426506331-add-payment-confirmation-in-order-detail-migration.ts diff --git a/src/order/entities/order.entity.ts b/src/order/entities/order.entity.ts index d002a07..799cf46 100644 --- a/src/order/entities/order.entity.ts +++ b/src/order/entities/order.entity.ts @@ -94,4 +94,12 @@ export class OrderDetail extends UUIDModel { (orderDeliveryDetail) => orderDeliveryDetail.orderDetail, ) orderDetailDeliveries: OrderDetailDelivery[]; + + @ManyToOne(() => PaymentConfirmation, { + nullable: true, + eager: true, + onDelete: 'SET NULL', + }) + @JoinColumn({ name: 'payment_confirmation_id' }) + paymentConfirmation: PaymentConfirmation; } diff --git a/src/order/migrations/1747426506331-add-payment-confirmation-in-order-detail-migration.ts b/src/order/migrations/1747426506331-add-payment-confirmation-in-order-detail-migration.ts new file mode 100644 index 0000000..5f1cbba --- /dev/null +++ b/src/order/migrations/1747426506331-add-payment-confirmation-in-order-detail-migration.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPaymentConfirmationInOrderDetailMigration1747426506331 + implements MigrationInterface +{ + name = 'AddPaymentConfirmationInOrderDetailMigration1747426506331'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "order_detail" ADD "payment_confirmation_id" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "order_detail" ADD CONSTRAINT "FK_2cb24b410baacf4bd387b22757e" FOREIGN KEY ("payment_confirmation_id") REFERENCES "payment_confirmation"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "order_detail" DROP CONSTRAINT "FK_2cb24b410baacf4bd387b22757e"`, + ); + await queryRunner.query( + `ALTER TABLE "order_detail" DROP COLUMN "payment_confirmation_id"`, + ); + } +} From 4944b7b8a08ad5a38f6a7741fdc73dddd1c9f664 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Fri, 16 May 2025 21:46:46 -0400 Subject: [PATCH 40/75] Fix inner dto for plain properties --- src/discount/controllers/coupon.controller.ts | 3 +- src/discount/dto/coupon.dto.ts | 25 +++++++--------- src/discount/services/coupon.service.ts | 10 ++----- .../product-presentation.controller.ts | 23 +-------------- src/products/products.controller.ts | 29 +++++++++++++++++++ src/user/dto/user.dto.ts | 21 +++++++------- src/user/user.controller.ts | 6 +++- src/user/user.service.ts | 12 +++----- 8 files changed, 66 insertions(+), 63 deletions(-) diff --git a/src/discount/controllers/coupon.controller.ts b/src/discount/controllers/coupon.controller.ts index a90051c..b09b0bd 100644 --- a/src/discount/controllers/coupon.controller.ts +++ b/src/discount/controllers/coupon.controller.ts @@ -142,7 +142,8 @@ export class CouponController { ): Promise { await this.couponService.bulkUpdate( couponListUpdateDTO.ids, - couponListUpdateDTO.data, + couponListUpdateDTO.maxUses, + couponListUpdateDTO.expirationDate, ); } diff --git a/src/discount/dto/coupon.dto.ts b/src/discount/dto/coupon.dto.ts index e01bd24..5f0048a 100644 --- a/src/discount/dto/coupon.dto.ts +++ b/src/discount/dto/coupon.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, PartialType, IntersectionType } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; +import { Transform, Expose } from 'class-transformer'; import { IsNotEmpty, IsString, @@ -84,26 +84,23 @@ export class CouponListDeleteDTO { ids: string[]; } -export class CouponBulkUpdateDTO { +export class CouponListUpdateDTO { + @IsUUID(undefined, { each: true }) + @ApiProperty({ + description: 'List of coupon ids to be updated', + type: [String], + }) + ids: string[]; + + @Expose() @IsOptional() @ApiProperty({ description: 'The new expiration date of the coupons' }) expirationDate: Date; + @Expose() @ApiProperty({ description: 'Maximum number of coupon uses' }) @IsOptional() @IsInt() @Min(0) maxUses: number; } - -export class CouponListUpdateDTO { - @IsUUID(undefined, { each: true }) - @ApiProperty({ - description: 'List of coupon ids to be updated', - type: [String], - }) - ids: string[]; - - @Type(() => CouponBulkUpdateDTO) - data: CouponBulkUpdateDTO; -} diff --git a/src/discount/services/coupon.service.ts b/src/discount/services/coupon.service.ts index d75c3be..79cdf70 100644 --- a/src/discount/services/coupon.service.ts +++ b/src/discount/services/coupon.service.ts @@ -2,11 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, IsNull, Repository } from 'typeorm'; import { Coupon } from '../entities/coupon.entity'; -import { - CouponBulkUpdateDTO, - CouponDTO, - UpdateCouponDTO, -} from '../dto/coupon.dto'; +import { CouponDTO, UpdateCouponDTO } from '../dto/coupon.dto'; @Injectable() export class CouponService { @@ -92,7 +88,7 @@ export class CouponService { await this.couponRepository.softDelete({ id: In(ids) }); } - async bulkUpdate(ids: string[], updateDto: CouponBulkUpdateDTO) { + async bulkUpdate(ids: string[], maxUses?: number, expirationDate?: Date) { const coupons = await this.couponRepository.findBy({ id: In(ids), deletedAt: IsNull(), @@ -101,7 +97,7 @@ export class CouponService { throw new NotFoundException(`No coupons found with the given IDs`); } const couponsToUpdate = coupons.map((coupon) => { - return { ...coupon, ...updateDto }; + return { ...coupon, maxUses, expirationDate }; }); await this.couponRepository.save(couponsToUpdate); } diff --git a/src/products/controllers/product-presentation.controller.ts b/src/products/controllers/product-presentation.controller.ts index 1c3470a..e41d393 100644 --- a/src/products/controllers/product-presentation.controller.ts +++ b/src/products/controllers/product-presentation.controller.ts @@ -13,10 +13,7 @@ import { UseGuards, } from '@nestjs/common'; import { ProductPresentationService } from '../services/product-presentation.service'; -import { - CreateProductPresentationDTO, - ProductPresentationListUpdateDTO, -} from '../dto/product-presentation.dto'; +import { CreateProductPresentationDTO } from '../dto/product-presentation.dto'; import { PresentationService } from '../services/presentation.service'; import { AuthGuard } from 'src/auth/auth.guard'; import { RolesGuard } from 'src/auth/roles.guard'; @@ -77,24 +74,6 @@ export class ProductPresentationController { ); } - @UseGuards(AuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @Patch('bulk') - @ApiBearerAuth() - @ApiOperation({ summary: 'Bulk update orders' }) - @ApiResponse({ - status: HttpStatus.NO_CONTENT, - description: 'Orders updated successfully', - }) - async bulkUpdate( - @Body() updateProductPresentationDto: ProductPresentationListUpdateDTO, - ): Promise { - await this.productPresentationService.bulkUpdate( - updateProductPresentationDto.ids, - updateProductPresentationDto.isVisible, - ); - } - @Get(':presentationId') @ApiOperation({ summary: 'Get product presentation by product and presentation ID', diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 819a971..a3978ac 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -1,6 +1,9 @@ import { + Body, Controller, Get, + HttpStatus, + Patch, Query, Req, UseGuards, @@ -8,10 +11,12 @@ import { } from '@nestjs/common'; import { ProductsService } from './products.service'; import { + ApiBearerAuth, ApiExtraModels, ApiOkResponse, ApiOperation, ApiQuery, + ApiResponse, getSchemaPath, } from '@nestjs/swagger'; import { ProductPresentationDTO, ProductQueryDTO } from './dto/product.dto'; @@ -20,6 +25,11 @@ import { PaginationInterceptor } from 'src/utils/pagination.interceptor'; import { plainToInstance } from 'class-transformer'; import { AuthGuard, CustomRequest } from 'src/auth/auth.guard'; import { RecommendationService } from 'src/recommendation/recommendation.service'; +import { RolesGuard } from 'src/auth/roles.guard'; +import { Roles } from 'src/auth/roles.decorador'; +import { UserRole } from 'src/user/entities/user.entity'; +import { ProductPresentationListUpdateDTO } from './dto/product-presentation.dto'; +import { ProductPresentationService } from './services/product-presentation.service'; @Controller('product') @ApiExtraModels(PaginationDTO, ProductPresentationDTO) @@ -27,6 +37,7 @@ export class ProductsController { constructor( private productsServices: ProductsService, private recommendationService: RecommendationService, + private productPresentationService: ProductPresentationService, ) {} @Get() @UseInterceptors(PaginationInterceptor) @@ -187,4 +198,22 @@ export class ProductsController { total: products.total, }; } + + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Patch('presentation/bulk') + @ApiBearerAuth() + @ApiOperation({ summary: 'Bulk update orders' }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Orders updated successfully', + }) + async bulkUpdate( + @Body() updateProductPresentationDto: ProductPresentationListUpdateDTO, + ): Promise { + await this.productPresentationService.bulkUpdate( + updateProductPresentationDto.ids, + updateProductPresentationDto.isVisible, + ); + } } diff --git a/src/user/dto/user.dto.ts b/src/user/dto/user.dto.ts index f7cf93a..d24ca4f 100644 --- a/src/user/dto/user.dto.ts +++ b/src/user/dto/user.dto.ts @@ -103,26 +103,27 @@ export class BaseUserDTO { export class UserDTO extends IntersectionType(BaseUserDTO, PasswordDTO) {} -export class UserBulkUpdateDTO { +export class UserListUpdateDTO { + @ApiProperty({ + description: 'List of user IDs to be updated', + type: [String], + }) + @ArrayNotEmpty() + @IsUUID(undefined, { each: true }) + users: string[]; + + @Expose() @IsOptional() @ApiProperty({ description: 'If the user has validated the email' }) isValidated: boolean; + @Expose() @IsOptional() @IsEnum(UserRole) @ApiProperty({ description: 'Role of the user', enum: UserRole }) role: UserRole; } -export class UserListUpdateDTO { - @ArrayNotEmpty() - @IsUUID(undefined, { each: true }) - users: string[]; - - @Type(() => UserBulkUpdateDTO) - data: UserBulkUpdateDTO; -} - export class UserAdminDTO extends BaseUserDTO { @ApiProperty({ description: 'the role of the user' }) @IsNotEmpty() diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 6c1ac9f..ea9e56f 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -86,7 +86,11 @@ export class UserController { description: 'Users updated successfully', }) async bulkUpdate(@Body() updateUserDto: UserListUpdateDTO): Promise { - await this.userService.bulkUpdate(updateUserDto.users, updateUserDto.data); + await this.userService.bulkUpdate( + updateUserDto.users, + updateUserDto.isValidated, + updateUserDto.role, + ); } @HttpCode(HttpStatus.OK) diff --git a/src/user/user.service.ts b/src/user/user.service.ts index fc2b362..b24d5c2 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -6,12 +6,7 @@ import { } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; -import { - UserAdminDTO, - UserDTO, - UpdateUserDTO, - UserBulkUpdateDTO, -} from './dto/user.dto'; +import { UserAdminDTO, UserDTO, UpdateUserDTO } from './dto/user.dto'; import { User, UserRole } from './entities/user.entity'; import { UserOTP } from './entities/user-otp.entity'; import { Profile } from './entities/profile.entity'; @@ -420,7 +415,8 @@ export class UserService { async bulkUpdate( userIds: string[], - userDto: UserBulkUpdateDTO, + isValidated?: boolean, + UserRole?: UserRole, ): Promise { const users = await this.userRepository.findBy({ id: In(userIds) }); if (!users.length) { @@ -428,7 +424,7 @@ export class UserService { } const updatedUsers = users.map((user) => { - return { ...user, ...userDto }; + return { ...user, isValidated, role: UserRole }; }); return await this.userRepository.save(updatedUsers); From 4f7f76814a49e28c02c9020643309635c2141866 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sat, 17 May 2025 16:35:45 -0400 Subject: [PATCH 41/75] Refactor and adjust permission of user by role --- src/discount/controllers/coupon.controller.ts | 16 +- src/discount/controllers/promo.controller.ts | 2 +- src/email/email.controller.spec.ts | 18 -- src/inventory/inventory.controller.ts | 29 ++- src/inventory/inventory.service.ts | 24 ++- .../controllers/order-delivery.controller.ts | 29 ++- src/order/controllers/order.controller.ts | 36 +++- src/order/order.gateway.ts | 8 +- src/order/order.module.ts | 2 +- .../services/order-delivery.controller.ts | 166 ++++++++++++++++ src/order/{ => services}/order.service.ts | 183 +++--------------- .../services/payment-confirmation.service.ts | 2 +- src/reports/reports.controller.ts | 18 +- 13 files changed, 322 insertions(+), 211 deletions(-) delete mode 100644 src/email/email.controller.spec.ts create mode 100644 src/order/services/order-delivery.controller.ts rename src/order/{ => services}/order.service.ts (78%) diff --git a/src/discount/controllers/coupon.controller.ts b/src/discount/controllers/coupon.controller.ts index b09b0bd..f26d040 100644 --- a/src/discount/controllers/coupon.controller.ts +++ b/src/discount/controllers/coupon.controller.ts @@ -43,7 +43,7 @@ export class CouponController { @Post() @UseGuards(AuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) + @Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) @ApiOperation({ summary: 'Create a coupon' }) @ApiResponse({ status: HttpStatus.CREATED, @@ -56,7 +56,7 @@ export class CouponController { @Get() @UseGuards(AuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) + @Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) @UseInterceptors(PaginationInterceptor) @ApiOperation({ summary: 'List all coupons' }) @ApiQuery({ @@ -118,7 +118,10 @@ export class CouponController { } @HttpCode(HttpStatus.NO_CONTENT) + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) @Delete('bulk') + @ApiBearerAuth() @ApiOperation({ summary: 'Bulk delete coupons' }) @ApiResponse({ description: 'Successful bulk deletion of coupons', @@ -131,7 +134,10 @@ export class CouponController { } @HttpCode(HttpStatus.NO_CONTENT) + @UseGuards(AuthGuard, RolesGuard) + @Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) @Patch('bulk') + @ApiBearerAuth() @ApiOperation({ summary: 'Bulk update coupons' }) @ApiResponse({ description: 'Successful bulk update of coupons', @@ -160,7 +166,8 @@ export class CouponController { @Patch(':code') @UseGuards(AuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) + @Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) + @ApiBearerAuth() @ApiOperation({ summary: 'Update coupon by code' }) @ApiResponse({ status: HttpStatus.OK, @@ -177,7 +184,8 @@ export class CouponController { @HttpCode(HttpStatus.NO_CONTENT) @Delete(':code') @UseGuards(AuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) + @Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) + @ApiBearerAuth() @ApiOperation({ summary: 'Delete coupon by code' }) @ApiResponse({ status: HttpStatus.NO_CONTENT, diff --git a/src/discount/controllers/promo.controller.ts b/src/discount/controllers/promo.controller.ts index 9cde9a1..9e7ad28 100644 --- a/src/discount/controllers/promo.controller.ts +++ b/src/discount/controllers/promo.controller.ts @@ -41,7 +41,7 @@ import { PaginationInterceptor } from 'src/utils/pagination.interceptor'; @ApiExtraModels(PaginationDTO, ResponsePromoDTO) @ApiBearerAuth() @UseGuards(AuthGuard, RolesGuard) -@Roles(UserRole.ADMIN) +@Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) export class PromoController { constructor(private readonly promoService: PromoService) {} diff --git a/src/email/email.controller.spec.ts b/src/email/email.controller.spec.ts deleted file mode 100644 index 9470aa8..0000000 --- a/src/email/email.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { EmailController } from './email.controller'; - -describe('EmailController', () => { - let controller: EmailController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [EmailController], - }).compile(); - - controller = module.get(EmailController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/inventory/inventory.controller.ts b/src/inventory/inventory.controller.ts index dc863bc..0bcdd35 100644 --- a/src/inventory/inventory.controller.ts +++ b/src/inventory/inventory.controller.ts @@ -11,6 +11,8 @@ import { HttpCode, Query, UseInterceptors, + Req, + BadRequestException, } from '@nestjs/common'; import { InventoryService } from './inventory.service'; import { @@ -21,7 +23,7 @@ import { BulkUpdateInventoryDTO, } from './dto/inventory.dto'; import { RolesGuard } from 'src/auth/roles.guard'; -import { AuthGuard } from 'src/auth/auth.guard'; +import { AuthGuard, CustomRequest } from 'src/auth/auth.guard'; import { UserRole } from 'src/user/entities/user.entity'; import { Roles } from 'src/auth/roles.decorador'; import { BranchId } from 'src/auth/branch-id.decorator'; @@ -53,8 +55,15 @@ export class InventoryController { type: ResponseInventoryDTO, }) async create( + @Req() req: CustomRequest, @Body() createInventoryDTO: CreateInventoryDTO, ): Promise { + if (req.user.role === UserRole.BRANCH_ADMIN) { + if (!req.user.branch) { + throw new BadRequestException('Branch not found'); + } + createInventoryDTO.branchId = req.user.branch.id; + } return await this.inventoryService.create(createInventoryDTO); } @@ -144,10 +153,15 @@ export class InventoryController { type: Inventory, }) async update( + @Req() req: CustomRequest, @Param('id') id: string, @Body() updateInventoryDTO: UpdateInventoryDTO, ) { - return await this.inventoryService.update(id, updateInventoryDTO); + let branchId: string | undefined; + if (req.user.role === UserRole.BRANCH_ADMIN) { + branchId = req.user.branch.id; + } + return await this.inventoryService.update(id, updateInventoryDTO, branchId); } @HttpCode(HttpStatus.NO_CONTENT) @@ -161,8 +175,15 @@ export class InventoryController { status: HttpStatus.NO_CONTENT, type: Inventory, }) - async remove(@Param('id') id: string): Promise { - await this.inventoryService.remove(id); + async remove( + @Req() req: CustomRequest, + @Param('id') id: string, + ): Promise { + let branchId: string | undefined; + if (req.user.role === UserRole.BRANCH_ADMIN) { + branchId = req.user.branch.id; + } + await this.inventoryService.remove(id, branchId); } @UseGuards(AuthGuard, RolesGuard) diff --git a/src/inventory/inventory.service.ts b/src/inventory/inventory.service.ts index dae18e3..fe4d61a 100644 --- a/src/inventory/inventory.service.ts +++ b/src/inventory/inventory.service.ts @@ -9,7 +9,7 @@ import { BulkUpdateInventoryDTO, } from './dto/inventory.dto'; import { Inventory } from './entities/inventory.entity'; -import { Repository } from 'typeorm'; +import { FindOneOptions, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { ProductPresentation } from 'src/products/entities/product-presentation.entity'; import { BranchService } from 'src/branch/branch.service'; @@ -111,14 +111,30 @@ export class InventoryService { async update( id: string, updateInventoryDTO: UpdateInventoryDTO, + branchId?: string, ): Promise { - const inventory = await this.findOne(id); + let where: FindOneOptions = { where: { id } }; + if (branchId) { + where = { + where: { id, branch: { id: branchId } }, + }; + } + const inventory = await this.inventoryRepository.findOne(where); const updatedInventory = { ...inventory, ...updateInventoryDTO }; return await this.inventoryRepository.save(updatedInventory); } - async remove(id: string): Promise { - const inventory = await this.findOne(id); + async remove(id: string, branchId?: string): Promise { + let where: FindOneOptions = { where: { id } }; + if (branchId) { + where = { + where: { id, branch: { id: branchId } }, + }; + } + const inventory = await this.inventoryRepository.findOne(where); + if (!inventory) { + throw new NotFoundException(`Inventory #${id} not found`); + } const deleted = await this.inventoryRepository.softDelete(inventory.id); if (!deleted.affected) { throw new NotFoundException(`Inventory #${id} not found`); diff --git a/src/order/controllers/order-delivery.controller.ts b/src/order/controllers/order-delivery.controller.ts index 286a66a..ebe97db 100644 --- a/src/order/controllers/order-delivery.controller.ts +++ b/src/order/controllers/order-delivery.controller.ts @@ -11,7 +11,7 @@ import { Param, Patch, } from '@nestjs/common'; -import { OrderService } from '../order.service'; +import { OrderService } from '../services/order.service'; import { AuthGuard, CustomRequest } from 'src/auth/auth.guard'; import { ApiBearerAuth, @@ -29,10 +29,14 @@ import { } from '../dto/order-delivery.dto'; import { User } from 'src/user/entities/user.entity'; import { plainToInstance } from 'class-transformer'; +import { OrderDeliveryService } from '../services/order-delivery.controller'; @Controller('delivery') export class OrderDeliveryController { - constructor(private readonly orderService: OrderService) {} + constructor( + private readonly orderService: OrderService, + private readonly orderDeliveryService: OrderDeliveryService, + ) {} @Get() @UseGuards(AuthGuard) @@ -92,12 +96,17 @@ export class OrderDeliveryController { @Query() pagination: OrderDeliveryQueryDTO, ): Promise<{ data: OrderDeliveryDTO[]; total: number }> { const { page, limit, status, branchId, employeeId } = pagination; - const data = await this.orderService.findAllOD(req.user, page, limit, { - status, - branchId, - employeeId, - }); - const total = await this.orderService.countDeliveries(req.user, { + const data = await this.orderDeliveryService.findAll( + req.user, + page, + limit, + { + status, + branchId, + employeeId, + }, + ); + const total = await this.orderDeliveryService.countDeliveries(req.user, { status, branchId, employeeId, @@ -121,7 +130,7 @@ export class OrderDeliveryController { async getDelivery( @Param('deliveryId') deliveryId: string, ): Promise { - const delivery = await this.orderService.getDelivery(deliveryId); + const delivery = await this.orderDeliveryService.findOne(deliveryId); return plainToInstance(OrderDeliveryDTO, delivery, { excludeExtraneousValues: true, @@ -140,7 +149,7 @@ export class OrderDeliveryController { @Body() updateDeliveryDto: UpdateDeliveryDTO, ): Promise { const user = req.user as User; - const updatedDelivery = await this.orderService.updateDelivery( + const updatedDelivery = await this.orderDeliveryService.update( user, deliveryId, updateDeliveryDto, diff --git a/src/order/controllers/order.controller.ts b/src/order/controllers/order.controller.ts index 3aefbd3..51ff5d0 100644 --- a/src/order/controllers/order.controller.ts +++ b/src/order/controllers/order.controller.ts @@ -14,7 +14,7 @@ import { Patch, NotFoundException, } from '@nestjs/common'; -import { OrderService } from '../order.service'; +import { OrderService } from '../services/order.service'; import { CreateOrderDTO, OrderListUpdateDTO, @@ -139,10 +139,14 @@ export class OrderController { @Req() req: CustomRequest, @Query() query: OrderQueryDTO, ): Promise<{ data: ResponseOrderDTO[]; total: number }> { - const { page, limit, userId, branchId, status, type } = query; + const { page, limit, userId, status, type } = query; + let { branchId } = query; let user; if ([UserRole.ADMIN, UserRole.BRANCH_ADMIN].includes(req.user.role)) { if (userId) user = userId; + if (req.user.role === UserRole.BRANCH_ADMIN) { + branchId = req.user.branch.id; + } } else { user = req.user.id; } @@ -164,7 +168,7 @@ export class OrderController { } @UseGuards(AuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) + @Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN) @Patch('bulk') @ApiBearerAuth() @ApiOperation({ summary: 'Bulk update orders' }) @@ -172,10 +176,18 @@ export class OrderController { status: HttpStatus.NO_CONTENT, description: 'Orders updated successfully', }) - async bulkUpdate(@Body() updateOrderDto: OrderListUpdateDTO): Promise { + async bulkUpdate( + @Req() req: CustomRequest, + @Body() updateOrderDto: OrderListUpdateDTO, + ): Promise { + let branchId: string | undefined; + if (req.user.role === UserRole.BRANCH_ADMIN) { + branchId = req.user.branch.id; + } await this.orderService.bulkUpdate( updateOrderDto.orders, updateOrderDto.status, + branchId, ); } @@ -195,6 +207,11 @@ export class OrderController { let order: Order; if ([UserRole.ADMIN, UserRole.BRANCH_ADMIN].includes(req.user.role)) { order = await this.orderService.findOne(id); + if (req.user.role === UserRole.BRANCH_ADMIN) { + if (order.branch.id !== req.user.branch.id) { + throw new NotFoundException('Order not found'); + } + } } else if ([UserRole.DELIVERY].includes(req.user.role)) { order = await this.orderService.findOne(id); if (order.type !== OrderType.DELIVERY) { @@ -226,9 +243,18 @@ export class OrderController { type: ResponseOrderDetailedDTO, }) async update( + @Req() req: CustomRequest, @Param('id', new ParseUUIDPipe()) id: string, @Body() updateOrderStatusDTO: UpdateOrderStatusDTO, ) { - return await this.orderService.update(id, updateOrderStatusDTO.status); + let branchId: string | undefined; + if (req.user.role === UserRole.BRANCH_ADMIN) { + branchId = req.user.branch.id; + } + return await this.orderService.update( + id, + updateOrderStatusDTO.status, + branchId, + ); } } diff --git a/src/order/order.gateway.ts b/src/order/order.gateway.ts index 72e0c38..f9f19ae 100644 --- a/src/order/order.gateway.ts +++ b/src/order/order.gateway.ts @@ -16,7 +16,7 @@ import { } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { UpdateOrderStatusWsDTO } from './dto/order'; -import { OrderService } from './order.service'; +import { OrderService } from './services/order.service'; import { AuthGuardWs } from 'src/auth/auth.guard'; import { AuthService } from 'src/auth/auth.service'; import { RolesGuardWs } from 'src/auth/roles.guard'; @@ -24,6 +24,7 @@ import { Roles } from 'src/auth/roles.decorador'; import { UserRole } from 'src/user/entities/user.entity'; import { WebsocketExceptionsFilter } from './ws.filters'; import { UpdateDeliveryWsDTO } from './dto/order-delivery.dto'; +import { OrderDeliveryService } from './services/order-delivery.controller'; @WebSocketGateway({ cors: { @@ -38,6 +39,7 @@ export class OrderGateway implements OnGatewayConnection, OnGatewayDisconnect { constructor( private readonly authService: AuthService, private readonly orderService: OrderService, + private readonly orderDeliveryService: OrderDeliveryService, ) {} handleConnection(client: Socket) { @@ -98,8 +100,8 @@ export class OrderGateway implements OnGatewayConnection, OnGatewayDisconnect { @ConnectedSocket() client: Socket, @MessageBody() data: UpdateDeliveryWsDTO, ) { - this.orderService - .getDelivery(data.id) + this.orderDeliveryService + .findOne(data.id) .then((delivery) => { this.orderService .getUserByOrderId(delivery.order.id) diff --git a/src/order/order.module.ts b/src/order/order.module.ts index f00cfbe..7a5e7a4 100644 --- a/src/order/order.module.ts +++ b/src/order/order.module.ts @@ -1,5 +1,5 @@ import { Module, forwardRef } from '@nestjs/common'; -import { OrderService } from './order.service'; +import { OrderService } from './services/order.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Order, OrderDetail } from './entities/order.entity'; import { BranchService } from 'src/branch/branch.service'; diff --git a/src/order/services/order-delivery.controller.ts b/src/order/services/order-delivery.controller.ts new file mode 100644 index 0000000..e78a28f --- /dev/null +++ b/src/order/services/order-delivery.controller.ts @@ -0,0 +1,166 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { User, UserRole } from 'src/user/entities/user.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { OrderDelivery } from '../entities/order_delivery.entity'; +import { UpdateDeliveryDTO } from '../dto/order-delivery.dto'; +import { UserService } from 'src/user/user.service'; + +@Injectable() +export class OrderDeliveryService { + constructor( + @InjectRepository(OrderDelivery) + private orderDeliveryRepository: Repository, + private userService: UserService, + ) {} + + async findAll( + user: User, + page: number, + pageSize: number, + filters?: { + status?: string; + branchId?: string; + employeeId?: string; + }, + ): Promise { + const query = this.orderDeliveryRepository + .createQueryBuilder('delivery') + .leftJoinAndSelect('delivery.order', 'order') + .leftJoinAndSelect('order.user', 'orderUser') + .leftJoinAndSelect('orderUser.profile', 'profile') + .leftJoinAndSelect('delivery.address', 'address') + .leftJoinAndSelect('address.city', 'city') + .leftJoinAndSelect('city.state', 'state') + .leftJoinAndSelect('state.country', 'country') + .leftJoinAndSelect('delivery.branch', 'branch') + .leftJoinAndSelect('delivery.employee', 'employee') + .where('delivery.deletedAt IS NULL'); + + if (user.role !== UserRole.ADMIN) { + query.andWhere('employee.id = :userId', { userId: user.id }); + } else if (filters?.employeeId) { + query.andWhere('employee.id = :employeeId', { + employeeId: filters.employeeId, + }); + } + + if (filters?.status) { + query.andWhere('delivery.deliveryStatus = :status', { + status: filters.status, + }); + } + + if (filters?.branchId) { + query.andWhere('branch.id = :branchId', { branchId: filters.branchId }); + } + + const delivery = query + .orderBy('delivery.createdAt', 'DESC') + .skip((page - 1) * pageSize) + .take(pageSize); + + return await delivery.getMany(); + } + + async countDeliveries( + user: User, + filters?: { + status?: string; + branchId?: string; + employeeId?: string; + }, + ): Promise { + const query = this.orderDeliveryRepository + .createQueryBuilder('delivery') + .leftJoin('delivery.branch', 'branch') + .leftJoin('delivery.employee', 'employee') + .where('delivery.deletedAt IS NULL'); + + if (user.role !== UserRole.ADMIN) { + query.andWhere('employee.id = :userId', { userId: user.id }); + } else if (filters?.employeeId) { + query.andWhere('employee.id = :employeeId', { + employeeId: filters.employeeId, + }); + } + + if (filters?.status) { + query.andWhere('delivery.deliveryStatus = :status', { + status: filters.status, + }); + } + + if (filters?.branchId) { + query.andWhere('branch.id = :branchId', { branchId: filters.branchId }); + } + + return await query.getCount(); + } + + async findOne(deliveryId: string): Promise { + const delivery = await this.orderDeliveryRepository.findOne({ + where: { id: deliveryId }, + relations: [ + 'order', + 'order.user', + 'order.user.profile', + 'address', + 'address.city', + 'address.city.state', + 'address.city.state.country', + 'employee', + 'branch', + ], + }); + if (!delivery) { + throw new NotFoundException('Delivery not found.'); + } + return delivery; + } + + async update( + user: User, + deliveryId: string, + updateData: UpdateDeliveryDTO, + ): Promise { + const delivery = await this.orderDeliveryRepository.findOne({ + where: { id: deliveryId }, + relations: [ + 'order', + 'order.user', + 'address', + 'address.city', + 'address.city.state', + 'address.city.state.country', + 'employee', + 'branch', + ], + }); + if (!delivery) { + throw new NotFoundException('Delivery not found.'); + } + + const updateDelivery = this.orderDeliveryRepository.merge( + delivery, + updateData, + ); + if (updateData.employeeId) { + const employee = await this.userService.findUserById( + updateData.employeeId, + ); + if (!employee) { + throw new NotFoundException('Employee not found.'); + } + if (employee.role !== UserRole.DELIVERY) { + throw new BadRequestException('User is not an employee.'); + } + updateDelivery.employee = employee; + } + return await this.orderDeliveryRepository.save(updateDelivery); + } +} diff --git a/src/order/order.service.ts b/src/order/services/order.service.ts similarity index 78% rename from src/order/order.service.ts rename to src/order/services/order.service.ts index bc41728..14c4b92 100644 --- a/src/order/order.service.ts +++ b/src/order/services/order.service.ts @@ -3,8 +3,8 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { CreateOrderDTO, SalesReportDTO } from './dto/order'; -import { User, UserRole } from 'src/user/entities/user.entity'; +import { CreateOrderDTO, SalesReportDTO } from '../dto/order'; +import { User } from 'src/user/entities/user.entity'; import { ProductPresentationService } from 'src/products/services/product-presentation.service'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; @@ -13,14 +13,13 @@ import { OrderDetail, OrderStatus, OrderType, -} from './entities/order.entity'; +} from '../entities/order.entity'; import { BranchService } from 'src/branch/branch.service'; import { Branch } from 'src/branch/entities/branch.entity'; import { OrderDelivery, OrderDeliveryStatus, -} from './entities/order_delivery.entity'; -import { UpdateDeliveryDTO } from './dto/order-delivery.dto'; +} from '../entities/order_delivery.entity'; import { UserAddress } from 'src/user/entities/user-address.entity'; import { UserService } from 'src/user/user.service'; import { CouponService } from 'src/discount/services/coupon.service'; @@ -198,9 +197,10 @@ export class OrderService { return { orders, total }; } - async findOne(id: string, userId?: string) { + async findOne(id: string, userId?: string, branchId?: string) { const where: Record = { id }; if (userId) where.user = { id: userId }; + if (branchId) where.branch = { id: branchId }; const order = await this.orderRepository.findOne({ where: where, relations: [ @@ -242,8 +242,12 @@ export class OrderService { return order; } - async update(id: string, status: OrderStatus): Promise { - const order = await this.findOne(id); + async update( + id: string, + status: OrderStatus, + branchId?: string, + ): Promise { + const order = await this.findOne(id, undefined, branchId); if (order.status === OrderStatus.COMPLETED) { console.log('A COMPLETED order cannot be modified'); return order; @@ -276,10 +280,19 @@ export class OrderService { return await this.orderRepository.save(order); } - async bulkUpdate(ordersIds: string[], status: OrderStatus) { - const orders = await this.orderRepository.findBy({ - id: In(ordersIds), - }); + async bulkUpdate( + ordersIds: string[], + status: OrderStatus, + branchId?: string, + ) { + let where: Record = { id: In(ordersIds) }; + if (branchId) { + where = { + ...where, + branch: { id: branchId }, + }; + } + const orders = await this.orderRepository.findBy(where); if (orders.length === 0) { throw new NotFoundException('No orders found'); } @@ -292,152 +305,6 @@ export class OrderService { return await this.orderRepository.save(updatedOrders); } - async findAllOD( - user: User, - page: number, - pageSize: number, - filters?: { - status?: string; - branchId?: string; - employeeId?: string; - }, - ): Promise { - const query = this.orderDeliveryRepository - .createQueryBuilder('delivery') - .leftJoinAndSelect('delivery.order', 'order') - .leftJoinAndSelect('order.user', 'orderUser') - .leftJoinAndSelect('orderUser.profile', 'profile') - .leftJoinAndSelect('delivery.address', 'address') - .leftJoinAndSelect('address.city', 'city') - .leftJoinAndSelect('city.state', 'state') - .leftJoinAndSelect('state.country', 'country') - .leftJoinAndSelect('delivery.branch', 'branch') - .leftJoinAndSelect('delivery.employee', 'employee') - .where('delivery.deletedAt IS NULL'); - - if (user.role !== UserRole.ADMIN) { - query.andWhere('employee.id = :userId', { userId: user.id }); - } else if (filters?.employeeId) { - query.andWhere('employee.id = :employeeId', { - employeeId: filters.employeeId, - }); - } - - if (filters?.status) { - query.andWhere('delivery.deliveryStatus = :status', { - status: filters.status, - }); - } - - if (filters?.branchId) { - query.andWhere('branch.id = :branchId', { branchId: filters.branchId }); - } - - const delivery = query - .orderBy('delivery.createdAt', 'DESC') - .skip((page - 1) * pageSize) - .take(pageSize); - - return await delivery.getMany(); - } - - async countDeliveries( - user: User, - filters?: { - status?: string; - branchId?: string; - employeeId?: string; - }, - ): Promise { - const query = this.orderDeliveryRepository - .createQueryBuilder('delivery') - .leftJoin('delivery.branch', 'branch') - .leftJoin('delivery.employee', 'employee') - .where('delivery.deletedAt IS NULL'); - - if (user.role !== UserRole.ADMIN) { - query.andWhere('employee.id = :userId', { userId: user.id }); - } else if (filters?.employeeId) { - query.andWhere('employee.id = :employeeId', { - employeeId: filters.employeeId, - }); - } - - if (filters?.status) { - query.andWhere('delivery.deliveryStatus = :status', { - status: filters.status, - }); - } - - if (filters?.branchId) { - query.andWhere('branch.id = :branchId', { branchId: filters.branchId }); - } - - return await query.getCount(); - } - - async getDelivery(deliveryId: string): Promise { - const delivery = await this.orderDeliveryRepository.findOne({ - where: { id: deliveryId }, - relations: [ - 'order', - 'order.user', - 'order.user.profile', - 'address', - 'address.city', - 'address.city.state', - 'address.city.state.country', - 'employee', - 'branch', - ], - }); - if (!delivery) { - throw new NotFoundException('Delivery not found.'); - } - return delivery; - } - - async updateDelivery( - user: User, - deliveryId: string, - updateData: UpdateDeliveryDTO, - ): Promise { - const delivery = await this.orderDeliveryRepository.findOne({ - where: { id: deliveryId }, - relations: [ - 'order', - 'order.user', - 'address', - 'address.city', - 'address.city.state', - 'address.city.state.country', - 'employee', - 'branch', - ], - }); - if (!delivery) { - throw new NotFoundException('Delivery not found.'); - } - - const updateDelivery = this.orderDeliveryRepository.merge( - delivery, - updateData, - ); - if (updateData.employeeId) { - const employee = await this.userService.findUserById( - updateData.employeeId, - ); - if (!employee) { - throw new NotFoundException('Employee not found.'); - } - if (employee.role !== UserRole.DELIVERY) { - throw new BadRequestException('User is not an employee.'); - } - updateDelivery.employee = employee; - } - return await this.orderDeliveryRepository.save(updateDelivery); - } - async countOrdersCompleted( status: OrderStatus, startDate: Date, diff --git a/src/payments/services/payment-confirmation.service.ts b/src/payments/services/payment-confirmation.service.ts index dabc264..0aed084 100644 --- a/src/payments/services/payment-confirmation.service.ts +++ b/src/payments/services/payment-confirmation.service.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { PaymentConfirmation } from '../entities/payment-confirmation.entity'; import { CreatePaymentConfirmationDTO } from '../dto/payment-confirmation.dto'; -import { OrderService } from 'src/order/order.service'; +import { OrderService } from 'src/order/services/order.service'; @Injectable() export class PaymentConfirmationService { diff --git a/src/reports/reports.controller.ts b/src/reports/reports.controller.ts index f7a35f1..a446b91 100644 --- a/src/reports/reports.controller.ts +++ b/src/reports/reports.controller.ts @@ -4,12 +4,13 @@ import { Query, BadRequestException, UseGuards, + Req, } from '@nestjs/common'; -import { OrderService } from 'src/order/order.service'; +import { OrderService } from 'src/order/services/order.service'; import { UserService } from 'src/user/user.service'; import { OrderStatus } from 'src/order/entities/order.entity'; import { Roles } from 'src/auth/roles.decorador'; -import { AuthGuard } from 'src/auth/auth.guard'; +import { AuthGuard, CustomRequest } from 'src/auth/auth.guard'; import { RolesGuard } from 'src/auth/roles.guard'; import { UserRole } from 'src/user/entities/user.entity'; import { ApiBearerAuth, ApiQuery, ApiResponse } from '@nestjs/swagger'; @@ -57,6 +58,7 @@ export class ReportsController { }, }) async getDashboard( + @Req() req: CustomRequest, @Query('startDate') start: string, @Query('endDate') end: string, @Query('branchId') branchId?: string, @@ -64,6 +66,9 @@ export class ReportsController { if (!start || !end) { throw new BadRequestException('startDate and endDate are required'); } + if (req.user.role === UserRole.BRANCH_ADMIN) { + branchId = req.user.branch.id; + } const startDate = new Date(start); const endDate = new Date(end); const [openOrders, completedOrders, totalSales, totalNewUsers] = @@ -86,6 +91,7 @@ export class ReportsController { totalNewUsers, }; } + @Get('order') @ApiBearerAuth() @ApiQuery({ @@ -120,10 +126,14 @@ export class ReportsController { }, }) async getOrdersByStatus( + @Req() req: CustomRequest, @Query('startDate') start: string, @Query('endDate') end: string, @Query('branchId') branchId?: string, ) { + if (req.user.role === UserRole.BRANCH_ADMIN) { + branchId = req.user.branch.id; + } if (!start || !end) { throw new BadRequestException('startDate and endDate are required'); } @@ -160,10 +170,14 @@ export class ReportsController { type: FullSalesReportDTO, }) async getSalesReport( + @Req() req: CustomRequest, @Query('startDate') start: string, @Query('endDate') end: string, @Query('branchId') branchId?: string, ) { + if (req.user.role === UserRole.BRANCH_ADMIN) { + branchId = req.user.branch.id; + } if (!start || !end) { throw new BadRequestException('startDate and endDate are required'); } From 844a456768eee513c401c50215747e245c4508d9 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sat, 17 May 2025 16:49:47 -0400 Subject: [PATCH 42/75] Fix missing order delivery service un order module --- src/order/order.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/order/order.module.ts b/src/order/order.module.ts index 7a5e7a4..05dc39f 100644 --- a/src/order/order.module.ts +++ b/src/order/order.module.ts @@ -26,6 +26,7 @@ import { CouponService } from 'src/discount/services/coupon.service'; import { InventoryModule } from 'src/inventory/inventory.module'; import { OrderGateway } from './order.gateway'; import { EmailModule } from 'src/email/email.module'; +import { OrderDeliveryService } from './services/order-delivery.controller'; @Module({ imports: [ @@ -57,6 +58,7 @@ import { EmailModule } from 'src/email/email.module'; PromoService, CouponService, OrderGateway, + OrderDeliveryService, ], exports: [OrderService], }) From ca4aa12a1634ae8fdcce19efa13caf13e8e6105f Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sat, 17 May 2025 17:34:11 -0400 Subject: [PATCH 43/75] Fix epoch times and add branch id to filter --- src/sales/sales.service.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/sales/sales.service.ts b/src/sales/sales.service.ts index bad7072..cd48143 100644 --- a/src/sales/sales.service.ts +++ b/src/sales/sales.service.ts @@ -12,15 +12,18 @@ export class SalesService { private readonly orderRepository: Repository, ) {} - async getDailySales(): Promise { - const sales = await this.orderRepository + async getDailySales(branchId?: string): Promise { + const query = this.orderRepository .createQueryBuilder('order') .select('DATE(order.createdAt)', 'date') .addSelect('SUM(order.totalPrice)', 'total') .where('order.status = :status', { status: OrderStatus.COMPLETED }) .groupBy('DATE(order.createdAt)') - .orderBy('DATE(order.createdAt)', 'ASC') - .getRawMany<{ date: string; total: string }>(); + .orderBy('DATE(order.createdAt)', 'ASC'); + if (branchId) { + query.andWhere('order.branchId = :branchId', { branchId }); + } + const sales = await query.getRawMany<{ date: string; total: string }>(); return sales.map((row) => ({ date: row.date, @@ -69,7 +72,7 @@ export class SalesService { model.add(tf.layers.dense({ inputShape: [1], units: 1 })); model.compile({ optimizer: 'sgd', loss: 'meanSquaredError' }); - await model.fit(xs, ys, { epochs: 300 }); + await model.fit(xs, ys, { epochs: 3 }); const futureDays = Array.from( { length: daysAhead }, From 391694a40b9b67c8ae4ccd77d5e413b46b47f6d2 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sat, 17 May 2025 17:39:26 -0400 Subject: [PATCH 44/75] Get user branch if the user is branch admin --- src/sales/sales.controller.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/sales/sales.controller.ts b/src/sales/sales.controller.ts index 5fd1472..edfda33 100644 --- a/src/sales/sales.controller.ts +++ b/src/sales/sales.controller.ts @@ -1,7 +1,7 @@ -import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; import { SalesService } from './sales.service'; import { PredictedSaleDTO, PredictSalesDTO } from './dto/predict-sales.dto'; -import { AuthGuard } from 'src/auth/auth.guard'; +import { AuthGuard, CustomRequest } from 'src/auth/auth.guard'; import { RolesGuard } from 'src/auth/roles.guard'; import { Roles } from 'src/auth/roles.decorador'; import { UserRole } from 'src/user/entities/user.entity'; @@ -37,10 +37,16 @@ export class SalesController { description: 'List of predicted daily sales values', type: [PredictedSaleDTO], }) - async predict(@Query() query: PredictSalesDTO): Promise { - const salesData = await this.salesService.getDailySales(); + async predict( + @Req() req: CustomRequest, + @Query() query: PredictSalesDTO, + ): Promise { + let branchId: string | undefined; + if (req.user.role === UserRole.BRANCH_ADMIN) { + branchId = req.user.branch.id; + } + const salesData = await this.salesService.getDailySales(branchId); const dailySales = this.salesService.fillMissingDates(salesData); - const days = query.days ? parseInt(query.days, 10) || 7 : 7; return await this.salesService.predictNext(days, dailySales); } From 3ea97d0cc2244fd87e8c85a1897b55b3c1866b73 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sat, 17 May 2025 20:24:07 -0400 Subject: [PATCH 45/75] add paymentConfirmation to ResponseOrderDetailDTO --- src/order/dto/order.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/order/dto/order.ts b/src/order/dto/order.ts index 1096e04..38a8c24 100644 --- a/src/order/dto/order.ts +++ b/src/order/dto/order.ts @@ -19,6 +19,7 @@ import { ResponseOrderProductPresentationDetailDTO } from 'src/products/dto/prod import { ResponseBranchDTO } from 'src/branch/dto/branch.dto'; import { OrderDeliveryEmployeeDTO } from './order-delivery.dto'; import { PaymentMethod } from 'src/payments/entities/payment-information.entity'; +import { ResponsePaymentConfirmationDTO } from 'src/payments/dto/payment-confirmation.dto'; export class CreateOrderDetailDTO { @ApiProperty({ description: 'ID of the product presentation' }) @@ -117,6 +118,15 @@ export class ResponseOrderDetailDTO { @Expose() @ApiProperty({ description: 'Subtotal price of the order detail' }) subtotal: number; + + @Expose() + @ApiProperty({ + description: 'Payment confirmation data (if any)', + type: ResponsePaymentConfirmationDTO, + required: false, + }) + @Type(() => ResponsePaymentConfirmationDTO) + paymentConfirmation?: ResponsePaymentConfirmationDTO; } export class ResponseOrderDTO extends BaseDTO { From 218d067873e3c448c6bc01d3e914d57f7f046298 Mon Sep 17 00:00:00 2001 From: Abraham <1001.28021547.ucla@gmail.com> Date: Sat, 17 May 2025 21:08:09 -0400 Subject: [PATCH 46/75] add price to ResponseOrderDetailDTO --- src/order/dto/order.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/order/dto/order.ts b/src/order/dto/order.ts index 57133c3..aaee166 100644 --- a/src/order/dto/order.ts +++ b/src/order/dto/order.ts @@ -116,6 +116,12 @@ export class ResponseOrderDetailDTO { @IsPositive() quantity: number; + @Expose() + @ApiProperty({ description: 'Product price' }) + @IsInt() + @IsPositive() + price: number; + @Expose() @ApiProperty({ description: 'Subtotal price of the order detail' }) subtotal: number; From f8b2fa321e3df8e4cd615eeb60fefb1b5f6e1a07 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 18 May 2025 13:18:10 -0400 Subject: [PATCH 47/75] Add uuid validation --- src/branch/dto/branch.dto.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/branch/dto/branch.dto.ts b/src/branch/dto/branch.dto.ts index afcc97f..86735ac 100644 --- a/src/branch/dto/branch.dto.ts +++ b/src/branch/dto/branch.dto.ts @@ -1,6 +1,13 @@ import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; -import { IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; +import { + IsNotEmpty, + IsNumber, + IsString, + IsUUID, + Max, + Min, +} from 'class-validator'; import { CityDTO } from 'src/city/dto/city.dto'; import { BaseDTO } from 'src/utils/dto/base.dto'; @@ -36,6 +43,7 @@ class BranchDTO { export class CreateBranchDTO extends BranchDTO { @IsNotEmpty() + @IsUUID() @ApiProperty({ description: 'The city id of the branch' }) cityId: string; } From 67f5ad662349471767de5c558e0ff01d03adcbb3 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 18 May 2025 14:35:50 -0400 Subject: [PATCH 48/75] Add default event --- src/main.ts | 1 + src/notification/services/notification.service.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index 391f259..8420a89 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,4 +31,5 @@ async function bootstrap() { SwaggerModule.setup('docs', app, documentFactory); await app.listen(process.env.PORT ?? 3000); } + bootstrap(); diff --git a/src/notification/services/notification.service.ts b/src/notification/services/notification.service.ts index 39f923a..973f197 100644 --- a/src/notification/services/notification.service.ts +++ b/src/notification/services/notification.service.ts @@ -2,7 +2,7 @@ import { Injectable, ForbiddenException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Notification } from '../entities/notification.entity'; -import { Subject, Observable, filter } from 'rxjs'; +import { Subject, Observable, filter, defaultIfEmpty } from 'rxjs'; import { MessageEvent } from '@nestjs/common'; @Injectable() @@ -54,6 +54,13 @@ export class NotificationService { subscribeToNotifications(userId: string): Observable { return this.notifications$ .asObservable() - .pipe(filter((evt) => evt.id === userId)); + .pipe(filter((evt) => evt.id == userId)) + .pipe( + defaultIfEmpty({ + data: 'empty', + id: 'empty', + type: 'empty', + } as MessageEvent), + ); } } From 40a8873c782cab25e424572556243be7c6e40d72 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 18 May 2025 21:44:02 -0400 Subject: [PATCH 49/75] Create dto to update coordinates of a delivery --- src/order/dto/order-delivery.dto.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/order/dto/order-delivery.dto.ts b/src/order/dto/order-delivery.dto.ts index c0156e5..365dc67 100644 --- a/src/order/dto/order-delivery.dto.ts +++ b/src/order/dto/order-delivery.dto.ts @@ -3,7 +3,14 @@ import { ApiPropertyOptional, IntersectionType, } from '@nestjs/swagger'; -import { IsDateString, IsOptional, IsString, IsUUID } from 'class-validator'; +import { + IsDateString, + IsLatitude, + IsLongitude, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; import { PaginationQueryDTO } from 'src/utils/dto/pagination.dto'; import { Expose, Transform, Type } from 'class-transformer'; import { BaseUserDTO } from 'src/user/dto/user.dto'; @@ -180,3 +187,17 @@ export class UpdateDeliveryWsDTO { @IsOptional() employeeId?: string; } + +export class UpdateCoordinatesWsDTO { + @ApiProperty({ description: 'ID of the order delivery' }) + @IsUUID() + orderId: string; + + @ApiProperty({ description: 'Latitude of the delivery location' }) + @IsLatitude() + latitude: number; + + @ApiProperty({ description: 'Longitude of the delivery location' }) + @IsLongitude() + longitude: number; +} From 1dc7560610eeff05a48a11311a70d5b03830df30 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Sun, 18 May 2025 21:44:29 -0400 Subject: [PATCH 50/75] Create event to update coordinates --- src/order/order.gateway.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/order/order.gateway.ts b/src/order/order.gateway.ts index f9f19ae..ec6fbbb 100644 --- a/src/order/order.gateway.ts +++ b/src/order/order.gateway.ts @@ -23,7 +23,10 @@ import { RolesGuardWs } from 'src/auth/roles.guard'; import { Roles } from 'src/auth/roles.decorador'; import { UserRole } from 'src/user/entities/user.entity'; import { WebsocketExceptionsFilter } from './ws.filters'; -import { UpdateDeliveryWsDTO } from './dto/order-delivery.dto'; +import { + UpdateCoordinatesWsDTO, + UpdateDeliveryWsDTO, +} from './dto/order-delivery.dto'; import { OrderDeliveryService } from './services/order-delivery.controller'; @WebSocketGateway({ @@ -122,4 +125,32 @@ export class OrderGateway implements OnGatewayConnection, OnGatewayDisconnect { client.to(client.id).emit('error', error); }); } + + @UseFilters(new WebsocketExceptionsFilter()) + @UsePipes( + new ValidationPipe({ + exceptionFactory: (errors) => new WsException(errors), + }), + ) + @UseGuards(AuthGuardWs, RolesGuardWs) + @Roles(UserRole.DELIVERY) + @SubscribeMessage('updateCoordinates') + updateCoordinates( + @ConnectedSocket() client: Socket, + @MessageBody() data: UpdateCoordinatesWsDTO, + ) { + this.orderService + .findOneWithUser(data.orderId) + .then((order) => { + if (order.user.wsId) { + client.to(order.user.wsId).emit('coordinatesUpdated', { + latitude: data.latitude, + longitude: data.longitude, + }); + } + }) + .catch((error) => { + client.to(client.id).emit('error', error); + }); + } } From 0990addb244bfcf3b244d5d3e30802a8605c694b Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Mon, 19 May 2025 10:08:13 -0400 Subject: [PATCH 51/75] Delete useless console log --- src/utils/pagination.interceptor.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/utils/pagination.interceptor.ts b/src/utils/pagination.interceptor.ts index 53e8342..6ba0e68 100644 --- a/src/utils/pagination.interceptor.ts +++ b/src/utils/pagination.interceptor.ts @@ -22,10 +22,7 @@ export class PaginationInterceptor implements NestInterceptor { next: CallHandler, ): Observable> { const req = context.switchToHttp().getRequest(); - const paginationQuery = plainToInstance(PaginationQueryDTO, req.query); - console.log('paginationQuery after transformation:', paginationQuery); - const errors = validateSync(paginationQuery); if (errors.length > 0) { @@ -35,8 +32,6 @@ export class PaginationInterceptor implements NestInterceptor { } req.pagination = paginationQuery; - console.log('Pagination received:', paginationQuery); - const { page, limit } = paginationQuery; const baseUrl = `${req.protocol}://${req.get('host')}${req.path}`; From 88c695219a01b52f2c4175bfcba56b5037bf4d56 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Mon, 19 May 2025 10:09:04 -0400 Subject: [PATCH 52/75] Update response inventory dto --- src/inventory/dto/inventory.dto.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/inventory/dto/inventory.dto.ts b/src/inventory/dto/inventory.dto.ts index af4c9b6..6476493 100644 --- a/src/inventory/dto/inventory.dto.ts +++ b/src/inventory/dto/inventory.dto.ts @@ -12,7 +12,8 @@ import { import { ResponseBranchDTO } from 'src/branch/dto/branch.dto'; import { BaseDTO } from 'src/utils/dto/base.dto'; import { PaginationQueryDTO } from 'src/utils/dto/pagination.dto'; -import { Type } from 'class-transformer'; +import { Expose, Type } from 'class-transformer'; +import { ProductPresentationDTO } from 'src/products/dto/product.dto'; export class InventoryDTO { @ApiProperty({ description: 'The stock quantity of the inventory' }) @@ -48,20 +49,21 @@ export class CreateInventoryDTO extends InventoryDTO { export class UpdateInventoryDTO extends PartialType(InventoryDTO) {} -class ProductPresentationDTO extends BaseDTO { - @ApiProperty({ description: 'The price of the product presentation' }) - price: number; -} - export class ResponseInventoryDTO extends IntersectionType( InventoryDTO, BaseDTO, ) { - @ApiProperty({ description: 'The branch in wich the product is available' }) + @Expose() + @ApiProperty({ + description: 'The branch in wich the product is available', + type: ResponseBranchDTO, + }) branch: ResponseBranchDTO; + @Expose() @ApiProperty({ description: 'The product presentation of the product in the inventory', + type: ProductPresentationDTO, }) productPresentation: ProductPresentationDTO; } From 3ca2456d84864e8254beda7a9d3a0dae5ef13a0c Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Mon, 19 May 2025 10:09:29 -0400 Subject: [PATCH 53/75] Replace query for querybuilder --- src/inventory/inventory.service.ts | 38 ++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/inventory/inventory.service.ts b/src/inventory/inventory.service.ts index fe4d61a..932bf8a 100644 --- a/src/inventory/inventory.service.ts +++ b/src/inventory/inventory.service.ts @@ -68,18 +68,32 @@ export class InventoryService { branchId?: string, productPresentationId?: string, ): Promise { - return await this.inventoryRepository.find({ - relations: ['branch', 'productPresentation'], - where: { - branch: branchId ? { id: branchId } : undefined, - productPresentation: productPresentationId - ? { id: productPresentationId } - : undefined, - }, - order: { createdAt: 'DESC' }, - skip: (page - 1) * pageSize, - take: pageSize, - }); + const query = this.inventoryRepository.createQueryBuilder('inventory'); + query + .innerJoinAndSelect('inventory.branch', 'branch') + .innerJoinAndSelect('branch.city', 'city') + .innerJoinAndSelect('city.state', 'state') + .innerJoinAndSelect('state.country', 'country') + .innerJoinAndSelect( + 'inventory.productPresentation', + 'productPresentation', + ) + .innerJoinAndSelect('productPresentation.product', 'product') + .innerJoinAndSelect('productPresentation.presentation', 'presentation') + .innerJoinAndSelect('product.manufacturer', 'manufacturer'); + if (branchId) { + query.andWhere('branch.id = :branchId', { branchId }); + } + if (productPresentationId) { + query.andWhere('productPresentation.id = :productPresentationId', { + productPresentationId, + }); + } + query.orderBy('inventory.createdAt', 'DESC'); + query.skip((page - 1) * pageSize); + query.take(pageSize); + const inventories = await query.getMany(); + return inventories; } async findOne(id: string): Promise { From 7b463e2e487a7463672961287a0964980e3c23a6 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Mon, 19 May 2025 10:09:53 -0400 Subject: [PATCH 54/75] Use plaint to instance to return inventories data --- src/inventory/inventory.controller.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/inventory/inventory.controller.ts b/src/inventory/inventory.controller.ts index 0bcdd35..52fdfb0 100644 --- a/src/inventory/inventory.controller.ts +++ b/src/inventory/inventory.controller.ts @@ -39,6 +39,7 @@ import { PaginationDTO } from 'src/utils/dto/pagination.dto'; import { PaginationInterceptor } from 'src/utils/pagination.interceptor'; import { PaginationQueryDTO } from 'src/utils/dto/pagination.dto'; import { Pagination } from 'src/utils/pagination.decorator'; +import { plainToInstance } from 'class-transformer'; @Controller('inventory') export class InventoryController { @@ -128,7 +129,13 @@ export class InventoryController { query.branchId, query.productPresentationId, ); - return { data, total }; + return { + data: plainToInstance(ResponseInventoryDTO, data, { + excludeExtraneousValues: true, + enableImplicitConversion: true, + }), + total, + }; } @Get(':id') From 861f71d1d87a9f98aa64a2059bbee8e74df4c394 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Mon, 19 May 2025 10:16:21 -0400 Subject: [PATCH 55/75] Expose stockQuantity property --- src/inventory/dto/inventory.dto.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/inventory/dto/inventory.dto.ts b/src/inventory/dto/inventory.dto.ts index 6476493..40a2f5a 100644 --- a/src/inventory/dto/inventory.dto.ts +++ b/src/inventory/dto/inventory.dto.ts @@ -16,6 +16,7 @@ import { Expose, Type } from 'class-transformer'; import { ProductPresentationDTO } from 'src/products/dto/product.dto'; export class InventoryDTO { + @Expose() @ApiProperty({ description: 'The stock quantity of the inventory' }) @IsNumber() @IsNotEmpty() From cba0f50d88f1b08603ac4e8141e8fd3c0616c297 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Mon, 19 May 2025 11:15:39 -0400 Subject: [PATCH 56/75] Fix city dto to expose all the properties --- src/branch/dto/branch.dto.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/branch/dto/branch.dto.ts b/src/branch/dto/branch.dto.ts index 86735ac..99677a5 100644 --- a/src/branch/dto/branch.dto.ts +++ b/src/branch/dto/branch.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; -import { Expose } from 'class-transformer'; +import { Expose, Type } from 'class-transformer'; import { IsNotEmpty, IsNumber, @@ -52,6 +52,7 @@ export class UpdateBranchDTO extends PartialType(CreateBranchDTO) {} export class ResponseBranchDTO extends IntersectionType(BranchDTO, BaseDTO) { @Expose() - @ApiProperty({ description: 'The city of the branch' }) + @ApiProperty({ description: 'The city of the branch', type: CityDTO }) + @Type(() => CityDTO) city: CityDTO; } From 8f77555c26b56bc00a103720cfd813510e4cf9af Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Mon, 19 May 2025 11:16:07 -0400 Subject: [PATCH 57/75] Add branch dto and missing mobile customer property to user dto --- src/user/dto/user.dto.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/user/dto/user.dto.ts b/src/user/dto/user.dto.ts index d24ca4f..b7522f2 100644 --- a/src/user/dto/user.dto.ts +++ b/src/user/dto/user.dto.ts @@ -23,6 +23,7 @@ import { import { UserGender } from '../entities/profile.entity'; import { IsOlderThan } from 'src/utils/is-older-than-validator'; import { UserRole } from '../entities/user.entity'; +import { ResponseBranchDTO } from 'src/branch/dto/branch.dto'; class PasswordDTO { @ApiProperty({ description: 'La contraseña del usuario' }) @@ -254,6 +255,13 @@ export class UserListDTO extends OmitType(UserDTO, ['birthDate'] as const) { @Expose() isValidated: boolean; + @Expose() + @ApiProperty({ + description: 'If the user has downloades and login in the mobile app', + }) + @IsOptional() + isMobileCustomer: boolean; + @ApiProperty({ description: 'Profile object' }) @Expose() @Type(() => ProfileDTO) @@ -263,6 +271,15 @@ export class UserListDTO extends OmitType(UserDTO, ['birthDate'] as const) { @Expose() @Type(() => UserMotoDTO) userMoto: UserMotoDTO; + + @Expose() + @ApiProperty({ + description: 'Branch object', + type: ResponseBranchDTO, + nullable: true, + }) + @Type(() => ResponseBranchDTO) + branch?: ResponseBranchDTO; } export class UpdateUserDTO extends PartialType( From fec15acbc905eac4d1e5ecf0bcd83e3ca1701d2f Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Mon, 19 May 2025 12:17:58 -0400 Subject: [PATCH 58/75] Use same filter to count --- src/inventory/inventory.controller.ts | 6 +----- src/inventory/inventory.service.ts | 21 +++------------------ 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/inventory/inventory.controller.ts b/src/inventory/inventory.controller.ts index 52fdfb0..00003c4 100644 --- a/src/inventory/inventory.controller.ts +++ b/src/inventory/inventory.controller.ts @@ -119,16 +119,12 @@ export class InventoryController { @Query() query: InventoryQueryDTO, ): Promise<{ data: ResponseInventoryDTO[]; total: number }> { const { page, limit } = pagination; - const data = await this.inventoryService.findAll( + const [data, total] = await this.inventoryService.findAll( page, limit, query.branchId, query.productPresentationId, ); - const total = await this.inventoryService.countInventories( - query.branchId, - query.productPresentationId, - ); return { data: plainToInstance(ResponseInventoryDTO, data, { excludeExtraneousValues: true, diff --git a/src/inventory/inventory.service.ts b/src/inventory/inventory.service.ts index 932bf8a..c14f9b0 100644 --- a/src/inventory/inventory.service.ts +++ b/src/inventory/inventory.service.ts @@ -47,27 +47,12 @@ export class InventoryService { return await this.inventoryRepository.save(inventory); } - async countInventories( - branchId?: string, - productPresentationId?: string, - ): Promise { - return await this.inventoryRepository.count({ - relations: ['branch', 'productPresentation'], - where: { - branch: branchId ? { id: branchId } : undefined, - productPresentation: productPresentationId - ? { id: productPresentationId } - : undefined, - }, - }); - } - async findAll( page: number, pageSize: number, branchId?: string, productPresentationId?: string, - ): Promise { + ): Promise<[Inventory[], number]> { const query = this.inventoryRepository.createQueryBuilder('inventory'); query .innerJoinAndSelect('inventory.branch', 'branch') @@ -92,8 +77,8 @@ export class InventoryService { query.orderBy('inventory.createdAt', 'DESC'); query.skip((page - 1) * pageSize); query.take(pageSize); - const inventories = await query.getMany(); - return inventories; + const [inventories, total] = await query.getManyAndCount(); + return [inventories, total]; } async findOne(id: string): Promise { From f3f0619c8e8267110ce6dd34ab1556a9edfe8326 Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Tue, 20 May 2025 21:53:25 -0400 Subject: [PATCH 59/75] csv upload is updated --- src/inventory/inventory.service.ts | 96 +++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 21 deletions(-) diff --git a/src/inventory/inventory.service.ts b/src/inventory/inventory.service.ts index c14f9b0..43da98c 100644 --- a/src/inventory/inventory.service.ts +++ b/src/inventory/inventory.service.ts @@ -175,31 +175,63 @@ export class InventoryService { ): Promise { const inventoriesToSave: Inventory[] = []; const movementsToSave: InventoryMovement[] = []; - //const lotsToSave: Lot[] = []; + + const totalByPresentation: Record = {}; + for (const item of bulkUpdateDto.inventories) { + totalByPresentation[item.productPresentationId] = + (totalByPresentation[item.productPresentationId] || 0) + item.quantity; + } + + const updatedPresentation: Record = {}; + + const originalStockMap: Record = {}; + for (const key of Object.keys(inventoryMap)) { + originalStockMap[key] = inventoryMap[key].stockQuantity; + } + + const lotMap = new Map< + string, + { quantity: number; expirationDate: Date } + >(); for (const item of bulkUpdateDto.inventories) { const inventory = inventoryMap[item.productPresentationId]; if (!inventory) continue; - inventory.stockQuantity = item.quantity; - inventoriesToSave.push(inventory); + if (!updatedPresentation[item.productPresentationId]) { + const originalQty = originalStockMap[item.productPresentationId]; + const newTotalQty = totalByPresentation[item.productPresentationId]; - const movement = this.inventoryMovementRepository.create({ - inventory, - quantity: item.quantity, - type: MovementType.IN, - }); - movementsToSave.push(movement); + inventory.stockQuantity = newTotalQty; + inventoriesToSave.push(inventory); + + const delta = newTotalQty - originalQty; + if (delta !== 0) { + const movement = this.inventoryMovementRepository.create({ + inventory, + quantity: Math.abs(delta), + type: delta > 0 ? MovementType.IN : MovementType.OUT, + }); + movementsToSave.push(movement); + } + updatedPresentation[item.productPresentationId] = true; + } - // if (item.expirationDate) { - // const lot = this.lotRepository.create({ - // productPresentation: { id: item.productPresentationId }, - // branch: { id: branchId }, - // quantity: item.quantity, - // expirationDate: new Date(item.expirationDate), - // }); - // lotsToSave.push(lot); - // } + if (item.expirationDate) { + const expDate = + item.expirationDate instanceof Date + ? item.expirationDate + : new Date(item.expirationDate); + const key = `${item.productPresentationId}_${expDate.toISOString()}`; + if (lotMap.has(key)) { + lotMap.get(key)!.quantity += item.quantity; + } else { + lotMap.set(key, { + quantity: item.quantity, + expirationDate: expDate, + }); + } + } } const updatedInventories = @@ -207,9 +239,31 @@ export class InventoryService { if (movementsToSave.length > 0) { await this.inventoryMovementRepository.save(movementsToSave); } - // if (lotsToSave.length > 0) { - // await this.lotRepository.save(lotsToSave); - // } + + for (const [key, info] of lotMap.entries()) { + const [presentationId] = key.split('_'); + + const existingLot = await this.lotRepository.findOne({ + where: { + productPresentation: { id: presentationId }, + branch: { id: branchId }, + expirationDate: info.expirationDate, + }, + }); + + if (existingLot) { + existingLot.quantity = info.quantity; + await this.lotRepository.save(existingLot); + } else { + const newLot = this.lotRepository.create({ + productPresentation: { id: presentationId }, + branch: { id: branchId }, + quantity: info.quantity, + expirationDate: info.expirationDate, + }); + await this.lotRepository.save(newLot); + } + } return updatedInventories; } From 58a1dc153ea11a78e3c416b0134d6ac28425082e Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Tue, 27 May 2025 16:53:54 -0400 Subject: [PATCH 60/75] Update relation between order and payment confirmation --- src/order/dto/order.ts | 18 ++++---- src/order/entities/order.entity.ts | 21 +++++---- .../entities/payment-confirmation.entity.ts | 4 +- ...13-update-one-to-one-relation-migration.ts | 43 +++++++++++++++++++ 4 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 src/payments/migrations/1748379101213-update-one-to-one-relation-migration.ts diff --git a/src/order/dto/order.ts b/src/order/dto/order.ts index aaee166..4a43918 100644 --- a/src/order/dto/order.ts +++ b/src/order/dto/order.ts @@ -125,15 +125,6 @@ export class ResponseOrderDetailDTO { @Expose() @ApiProperty({ description: 'Subtotal price of the order detail' }) subtotal: number; - - @Expose() - @ApiProperty({ - description: 'Payment confirmation data (if any)', - type: ResponsePaymentConfirmationDTO, - required: false, - }) - @Type(() => ResponsePaymentConfirmationDTO) - paymentConfirmation?: ResponsePaymentConfirmationDTO; } export class ResponseOrderDTO extends BaseDTO { @@ -157,6 +148,15 @@ export class ResponseOrderDTO extends BaseDTO { @IsEnum(PaymentMethod) @IsOptional() paymentMethod: PaymentMethod; + + @Expose() + @ApiProperty({ + description: 'Payment confirmation data (if any)', + type: ResponsePaymentConfirmationDTO, + required: false, + }) + @Type(() => ResponsePaymentConfirmationDTO) + paymentConfirmation?: ResponsePaymentConfirmationDTO; } export class ResponseOrderDetailedDTO extends ResponseOrderDTO { diff --git a/src/order/entities/order.entity.ts b/src/order/entities/order.entity.ts index 799cf46..ff7b25e 100644 --- a/src/order/entities/order.entity.ts +++ b/src/order/entities/order.entity.ts @@ -2,7 +2,14 @@ import { Branch } from 'src/branch/entities/branch.entity'; import { ProductPresentation } from 'src/products/entities/product-presentation.entity'; import { User } from 'src/user/entities/user.entity'; import { BaseModel, UUIDModel } from 'src/utils/entity'; -import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, +} from 'typeorm'; import { OrderDelivery, OrderDetailDelivery } from './order_delivery.entity'; import { PaymentMethod } from 'src/payments/entities/payment-information.entity'; import { PaymentConfirmation } from 'src/payments/entities/payment-confirmation.entity'; @@ -58,11 +65,11 @@ export class Order extends BaseModel { }) paymentMethod: PaymentMethod; - @OneToMany( + @OneToOne( () => PaymentConfirmation, (paymentConfirmation) => paymentConfirmation.order, ) - paymentConfirmations: PaymentConfirmation[]; + paymentConfirmation: PaymentConfirmation; } @Entity() @@ -94,12 +101,4 @@ export class OrderDetail extends UUIDModel { (orderDeliveryDetail) => orderDeliveryDetail.orderDetail, ) orderDetailDeliveries: OrderDetailDelivery[]; - - @ManyToOne(() => PaymentConfirmation, { - nullable: true, - eager: true, - onDelete: 'SET NULL', - }) - @JoinColumn({ name: 'payment_confirmation_id' }) - paymentConfirmation: PaymentConfirmation; } diff --git a/src/payments/entities/payment-confirmation.entity.ts b/src/payments/entities/payment-confirmation.entity.ts index 0e98477..f41e688 100644 --- a/src/payments/entities/payment-confirmation.entity.ts +++ b/src/payments/entities/payment-confirmation.entity.ts @@ -1,6 +1,6 @@ import { Order } from 'src/order/entities/order.entity'; import { BaseModel } from 'src/utils/entity'; -import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; +import { Column, Entity, JoinColumn, OneToOne } from 'typeorm'; @Entity('payment_confirmation') export class PaymentConfirmation extends BaseModel { @@ -16,7 +16,7 @@ export class PaymentConfirmation extends BaseModel { @Column({ type: 'character varying', name: 'phone_number' }) phoneNumber: string; - @ManyToOne(() => Order, (order) => order.paymentConfirmations, { + @OneToOne(() => Order, (order) => order.paymentConfirmation, { onDelete: 'RESTRICT', }) @JoinColumn({ name: 'order_id' }) diff --git a/src/payments/migrations/1748379101213-update-one-to-one-relation-migration.ts b/src/payments/migrations/1748379101213-update-one-to-one-relation-migration.ts new file mode 100644 index 0000000..8203577 --- /dev/null +++ b/src/payments/migrations/1748379101213-update-one-to-one-relation-migration.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateOneToOneRelationMigration1748379101213 + implements MigrationInterface +{ + name = 'UpdateOneToOneRelationMigration1748379101213'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "order_detail" DROP CONSTRAINT "FK_2cb24b410baacf4bd387b22757e"`, + ); + await queryRunner.query( + `ALTER TABLE "order_detail" DROP COLUMN "payment_confirmation_id"`, + ); + await queryRunner.query( + `ALTER TABLE "payment_confirmation" DROP CONSTRAINT "FK_de3ea608b9f32c2184b551c554b"`, + ); + await queryRunner.query( + `ALTER TABLE "payment_confirmation" ADD CONSTRAINT "UQ_de3ea608b9f32c2184b551c554b" UNIQUE ("order_id")`, + ); + await queryRunner.query( + `ALTER TABLE "payment_confirmation" ADD CONSTRAINT "FK_de3ea608b9f32c2184b551c554b" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "payment_confirmation" DROP CONSTRAINT "FK_de3ea608b9f32c2184b551c554b"`, + ); + await queryRunner.query( + `ALTER TABLE "payment_confirmation" DROP CONSTRAINT "UQ_de3ea608b9f32c2184b551c554b"`, + ); + await queryRunner.query( + `ALTER TABLE "payment_confirmation" ADD CONSTRAINT "FK_de3ea608b9f32c2184b551c554b" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "order_detail" ADD "payment_confirmation_id" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "order_detail" ADD CONSTRAINT "FK_2cb24b410baacf4bd387b22757e" FOREIGN KEY ("payment_confirmation_id") REFERENCES "payment_confirmation"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } +} From 74f0a87c1a41ea0c03c05217b9e98c8f23e61eb2 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Tue, 27 May 2025 17:04:33 -0400 Subject: [PATCH 61/75] Add relation in query of order detail --- src/order/services/order.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/order/services/order.service.ts b/src/order/services/order.service.ts index 14c4b92..6ffd9f5 100644 --- a/src/order/services/order.service.ts +++ b/src/order/services/order.service.ts @@ -212,6 +212,7 @@ export class OrderService { 'details.productPresentation.product.images', 'details.productPresentation.presentation', 'orderDeliveries.employee', + 'paymentConfirmation', ], }); if (!order) { @@ -232,6 +233,7 @@ export class OrderService { 'details.productPresentation.product.images', 'details.productPresentation.presentation', 'orderDeliveries.employee', + 'paymentConfirmation', 'user', 'user.profile', ], From 032db751e8409626288220aab33b6b04d60f076c Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Tue, 27 May 2025 17:49:34 -0400 Subject: [PATCH 62/75] Add discount to order detail --- src/order/entities/order.entity.ts | 3 +++ ...romo-discount-to-order-detail-migration.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/order/migrations/1748382133862-add-promo-discount-to-order-detail-migration.ts diff --git a/src/order/entities/order.entity.ts b/src/order/entities/order.entity.ts index ff7b25e..b210278 100644 --- a/src/order/entities/order.entity.ts +++ b/src/order/entities/order.entity.ts @@ -96,6 +96,9 @@ export class OrderDetail extends UUIDModel { @Column({ type: 'int', name: 'subtotal' }) subtotal: number; + @Column({ type: 'int', default: 0 }) + discount: number; + @OneToMany( () => OrderDetailDelivery, (orderDeliveryDetail) => orderDeliveryDetail.orderDetail, diff --git a/src/order/migrations/1748382133862-add-promo-discount-to-order-detail-migration.ts b/src/order/migrations/1748382133862-add-promo-discount-to-order-detail-migration.ts new file mode 100644 index 0000000..b5a76f0 --- /dev/null +++ b/src/order/migrations/1748382133862-add-promo-discount-to-order-detail-migration.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPromoDiscountToOrderDetailMigration1748382133862 + implements MigrationInterface +{ + name = 'AddPromoDiscountToOrderDetailMigration1748382133862'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "order_detail" ADD "discount" integer NOT NULL DEFAULT '0'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "order_detail" DROP COLUMN "discount"`, + ); + } +} From 3d34ae4717ff55d3fe83972e386d0a7d6ee66d1b Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Wed, 28 May 2025 18:34:49 -0400 Subject: [PATCH 63/75] Add discount and subtotal with discount --- src/order/services/order.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/order/services/order.service.ts b/src/order/services/order.service.ts index 6ffd9f5..440d5b84 100644 --- a/src/order/services/order.service.ts +++ b/src/order/services/order.service.ts @@ -128,12 +128,16 @@ export class OrderService { }); const order = await this.orderRepository.save(orderToCreate); const orderDetails = productsWithQuantity.map((product) => { + const priceWithDiscount = product.promo + ? product.price - (product.price * product.promo.discount) / 100 + : product.price; return this.orderDetailRepository.create({ order, productPresentation: product, quantity: product.quantity, price: product.price, - subtotal: product.price * product.quantity, + subtotal: Math.round(priceWithDiscount) * product.quantity, + discount: product.promo ? product.promo.discount : 0, }); }); await this.orderDetailRepository.save(orderDetails); From dc99d974530c9065418547053f29ec7a03168545 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Wed, 28 May 2025 18:56:30 -0400 Subject: [PATCH 64/75] Add discount field to dto --- src/order/dto/order.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/order/dto/order.ts b/src/order/dto/order.ts index 4a43918..604d1ca 100644 --- a/src/order/dto/order.ts +++ b/src/order/dto/order.ts @@ -125,6 +125,10 @@ export class ResponseOrderDetailDTO { @Expose() @ApiProperty({ description: 'Subtotal price of the order detail' }) subtotal: number; + + @Expose() + @ApiProperty({ description: 'Discount applied to the order detail' }) + discount: number; } export class ResponseOrderDTO extends BaseDTO { From a5c61e5c2b3303eb63689496db94c26229b6e5ac Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 09:25:42 -0400 Subject: [PATCH 65/75] Use query builder to properly filter by search query --- src/products/products.service.ts | 96 +++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/products/products.service.ts b/src/products/products.service.ts index b797ae2..36a163e 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In, Between, ILike } from 'typeorm'; +import { Repository, In, Between, ILike, Brackets } from 'typeorm'; import { ProductPresentation } from './entities/product-presentation.entity'; @Injectable() @@ -10,6 +10,100 @@ export class ProductsService { private productPresentationRepository: Repository, ) {} + async getProductQueryBuilder( + page: number, + limit: number, + searchQuery?: string, + categoryIds?: string[], + manufacturerIds?: string[], + branchIds?: string[], + presentationIds?: string[], + genericProductIds?: string[], + priceRange?: number[], + isVisible?: boolean, + ids?: string[], + ) { + const query = this.productPresentationRepository.createQueryBuilder( + 'product_presentation', + ); + query + .innerJoinAndSelect('product_presentation.product', 'product') + .innerJoinAndSelect('product.images', 'images') + .innerJoinAndSelect('product.manufacturer', 'manufacturer') + .innerJoinAndSelect('product.categories', 'categories') + .innerJoinAndSelect('product_presentation.presentation', 'presentation') + .leftJoinAndSelect('product_presentation.promo', 'promo'); + + if (searchQuery) { + query.where( + new Brackets((qb) => { + qb.where('product.name ILIKE :searchQuery', { + searchQuery: `%${searchQuery}%`, + }) + .orWhere('product.genericName ILIKE :searchQuery', { + searchQuery: `%${searchQuery}%`, + }) + .orWhere('product.description ILIKE :searchQuery', { + searchQuery: `%${searchQuery}%`, + }); + }), + ); + } + if (ids && ids.length > 0) { + query.andWhere('product_presentation.id IN (:...ids)', { ids }); + } + if (categoryIds && categoryIds.length > 0) { + query.andWhere('product.categories.id IN (:...categoryIds)', { + categoryIds, + }); + } + if (manufacturerIds && manufacturerIds.length > 0) { + query.andWhere('product.manufacturer.id IN (:...manufacturerIds)', { + manufacturerIds, + }); + } + if (branchIds && branchIds.length > 0) { + query + .innerJoinAndSelect('product_presentation.inventories', 'inventories') + .andWhere('inventories.branch.id IN (:...branchIds)', { branchIds }); + } + if (presentationIds && presentationIds.length > 0) { + query.andWhere( + 'product_presentation.presentation.id IN (:...presentationIds)', + { + presentationIds, + }, + ); + } + if (genericProductIds && genericProductIds.length > 0) { + query.andWhere('product.id IN (:...genericProductIds)', { + genericProductIds, + }); + } + if (priceRange && priceRange.length === 2) { + query.andWhere( + 'product_presentation.price BETWEEN :minPrice AND :maxPrice', + { + minPrice: priceRange[0], + maxPrice: priceRange[1], + }, + ); + } + if (typeof isVisible === 'boolean') { + query.andWhere('product_presentation.isVisible = :isVisible', { + isVisible: isVisible, + }); + } else { + query.andWhere('product_presentation.isVisible = true'); + } + query + .orderBy('product_presentation.createdAt', 'DESC') + .skip((page - 1) * limit) + .take(limit); + const [products, total] = await query.getManyAndCount(); + return { products, total }; + } + async getProducts( page: number, limit: number, From 5311cb0835544b72c5c585f33d75736d3c3cdd5d Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 09:27:13 -0400 Subject: [PATCH 66/75] Create lot endpoint to list all --- src/products/controllers/lot.controller.ts | 93 ++++++++++++++++++++++ src/products/dto/lot.dto.ts | 43 ++++++++++ src/products/services/lot.service.ts | 45 +++++++++++ 3 files changed, 181 insertions(+) create mode 100644 src/products/controllers/lot.controller.ts create mode 100644 src/products/dto/lot.dto.ts create mode 100644 src/products/services/lot.service.ts diff --git a/src/products/controllers/lot.controller.ts b/src/products/controllers/lot.controller.ts new file mode 100644 index 0000000..1bccda6 --- /dev/null +++ b/src/products/controllers/lot.controller.ts @@ -0,0 +1,93 @@ +import { + Controller, + Get, + HttpStatus, + Query, + UseInterceptors, +} from '@nestjs/common'; +import { LotService } from '../services/lot.service'; +import { PaginationInterceptor } from 'src/utils/pagination.interceptor'; +import { + ApiOperation, + ApiQuery, + ApiResponse, + getSchemaPath, +} from '@nestjs/swagger'; +import { + PaginationDTO, + PaginationQueryDTO, +} from 'src/utils/dto/pagination.dto'; +import { Pagination } from 'src/utils/pagination.decorator'; +import { plainToInstance } from 'class-transformer'; +import { LotQueryDTO, ResponseLotDTO } from '../dto/lot.dto'; + +@Controller('lot') +export class LotController { + constructor(private readonly lotService: LotService) {} + + @Get() + @UseInterceptors(PaginationInterceptor) + @ApiOperation({ summary: 'List all lots' }) + @ApiQuery({ + name: 'page', + required: false, + description: 'Page number for pagination', + type: Number, + example: 1, + }) + @ApiQuery({ + name: 'limit', + required: false, + description: 'Number of items per page', + type: Number, + example: 10, + }) + @ApiQuery({ + name: 'branchId', + required: false, + description: 'Filter by branch ID', + type: String, + }) + @ApiQuery({ + name: 'productPresentationId', + required: false, + description: 'Filter by product presentation ID', + type: String, + }) + @ApiResponse({ + description: 'Successful retrieval of lots', + status: HttpStatus.OK, + schema: { + allOf: [ + { $ref: getSchemaPath(PaginationDTO) }, + { + properties: { + results: { + type: 'array', + items: { $ref: getSchemaPath(ResponseLotDTO) }, + }, + }, + }, + ], + }, + }) + async findAll( + @Pagination() pagination: PaginationQueryDTO, + @Query() query: LotQueryDTO, + ): Promise<{ data: ResponseLotDTO[]; total: number }> { + const { page, limit } = pagination; + const [data, total] = await this.lotService.findAll( + page, + limit, + query.branchId, + query.productPresentationId, + ); + return { + data: plainToInstance(ResponseLotDTO, data, { + excludeExtraneousValues: true, + enableImplicitConversion: true, + }), + total, + }; + } +} diff --git a/src/products/dto/lot.dto.ts b/src/products/dto/lot.dto.ts new file mode 100644 index 0000000..9d5c631 --- /dev/null +++ b/src/products/dto/lot.dto.ts @@ -0,0 +1,43 @@ +import { ApiProperty, IntersectionType } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; +import { IsOptional, IsUUID } from 'class-validator'; +import { ResponseBranchDTO } from 'src/branch/dto/branch.dto'; +import { BaseDTO } from 'src/utils/dto/base.dto'; +import { PaginationQueryDTO } from 'src/utils/dto/pagination.dto'; +import { ProductPresentationDTO } from './product.dto'; + +export class LotDTO { + @Expose() + quantity: number; + + @Expose() + expirationDate: Date; +} + +export class LotQueryDTO extends PaginationQueryDTO { + @ApiProperty({ description: 'The branch id of the lot' }) + @IsOptional() + @IsUUID() + branchId: string; + + @ApiProperty({ description: 'The product presentation id of the lot' }) + @IsOptional() + @IsUUID() + productPresentationId: string; +} + +export class ResponseLotDTO extends IntersectionType(LotDTO, BaseDTO) { + @Expose() + @ApiProperty({ + description: 'The branch in wich the product is available', + type: ResponseBranchDTO, + }) + branch: ResponseBranchDTO; + + @Expose() + @ApiProperty({ + description: 'The product presentation of the product in the lot', + type: ProductPresentationDTO, + }) + productPresentation: ProductPresentationDTO; +} diff --git a/src/products/services/lot.service.ts b/src/products/services/lot.service.ts new file mode 100644 index 0000000..c7d6e17 --- /dev/null +++ b/src/products/services/lot.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { Repository } from 'typeorm'; +import { Lot } from '../entities/lot.entity'; +import { InjectRepository } from '@nestjs/typeorm'; + +@Injectable() +export class LotService { + constructor( + @InjectRepository(Lot) + private lotRepository: Repository, + ) {} + + async findAll( + page: number, + limit: number, + branchId?: string, + productPresentationId?: string, + ): Promise<[Lot[], number]> { + const query = this.lotRepository + .createQueryBuilder('lot') + .innerJoinAndSelect('lot.productPresentation', 'productPresentation') + .innerJoinAndSelect('lot.branch', 'branch') + .innerJoinAndSelect('branch.city', 'city') + .innerJoinAndSelect('city.state', 'state') + .innerJoinAndSelect('state.country', 'country') + .innerJoinAndSelect('productPresentation.product', 'product') + .innerJoinAndSelect('productPresentation.presentation', 'presentation') + .innerJoinAndSelect('product.manufacturer', 'manufacturer'); + + if (branchId) { + query.andWhere('lot.branchId = :branchId', { branchId }); + } + + if (productPresentationId) { + query.andWhere('lot.productPresentationId = :productPresentationId', { + productPresentationId, + }); + } + query.skip((page - 1) * limit); + query.take(limit); + query.orderBy('lot.createdAt', 'DESC'); + const [lots, count] = await query.getManyAndCount(); + return [lots, count]; + } +} From ae72e3fdafc67569c2babad817487cc34170b3fa Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 09:27:42 -0400 Subject: [PATCH 67/75] Update find all to use query builder version --- src/products/products.controller.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index a3978ac..c9d377a 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -151,19 +151,20 @@ export class ProductsController { isVisible, id, } = pagination; - const { products, total } = await this.productsServices.getProducts( - page, - limit, - q, - categoryId, - manufacturerId, - branchId, - presentationId, - genericProductId, - priceRange, - isVisible, - id, - ); + const { products, total } = + await this.productsServices.getProductQueryBuilder( + page, + limit, + q, + categoryId, + manufacturerId, + branchId, + presentationId, + genericProductId, + priceRange, + isVisible, + id, + ); return { data: plainToInstance(ProductPresentationDTO, products, { From 1a590efa2597aeb35c0b34eb04ecc71651b4a8cb Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 09:28:04 -0400 Subject: [PATCH 68/75] Import new lot service and controller --- src/products/products.module.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/products/products.module.ts b/src/products/products.module.ts index 4526f3c..2e50739 100644 --- a/src/products/products.module.ts +++ b/src/products/products.module.ts @@ -26,6 +26,9 @@ import { ProductCategoryController } from './controllers/product-category.contro import { ProductCategoryService } from './services/product-category.service'; import { CategoryService } from 'src/category/category.service'; import { RecommendationService } from 'src/recommendation/recommendation.service'; +import { LotService } from './services/lot.service'; +import { LotController } from './controllers/lot.controller'; +import { Lot } from './entities/lot.entity'; @Module({ imports: [ @@ -37,6 +40,7 @@ import { RecommendationService } from 'src/recommendation/recommendation.service Country, Category, ProductImage, + Lot, ]), AuthModule, forwardRef(() => DiscountModule), @@ -49,6 +53,7 @@ import { RecommendationService } from 'src/recommendation/recommendation.service ProductPresentationController, ProductImageController, ProductCategoryController, + LotController, ], providers: [ CategoryService, @@ -61,6 +66,7 @@ import { RecommendationService } from 'src/recommendation/recommendation.service ProductImageService, ProductCategoryService, RecommendationService, + LotService, ], }) export class ProductsModule {} From bd5018abada2d3753e3d3ed7f5f400e743ce4e38 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 09:52:38 -0400 Subject: [PATCH 69/75] Validate if the promo is expired --- src/order/services/order.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/order/services/order.service.ts b/src/order/services/order.service.ts index 440d5b84..0053a7a 100644 --- a/src/order/services/order.service.ts +++ b/src/order/services/order.service.ts @@ -104,9 +104,10 @@ export class OrderService { } let totalPrice = productsWithQuantity.reduce((acc, product) => { - const price = product.promo - ? product.price - (product.price * product.promo.discount) / 100 - : product.price; + const price = + product.promo && product.promo.expiredAt > new Date() + ? product.price - (product.price * product.promo.discount) / 100 + : product.price; return acc + Math.round(price) * product.quantity; }, 0); From abd0f2485e7cc7290687e493bb8c108881472df7 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 11:07:11 -0400 Subject: [PATCH 70/75] Fix subtotal in sales report --- src/order/services/order.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/order/services/order.service.ts b/src/order/services/order.service.ts index 0053a7a..538e1ff 100644 --- a/src/order/services/order.service.ts +++ b/src/order/services/order.service.ts @@ -437,7 +437,7 @@ export class OrderService { .addSelect('o.createdAt', 'date') .addSelect('o.type', 'type') .addSelect('SUM(d.quantity)', 'quantity') - .addSelect('SUM(d.subtotal)', 'subtotal') + .addSelect('SUM(d.quantity * d.price)', 'subtotal') .addSelect('SUM(d.quantity * d.price) - o.totalPrice', 'discount') .addSelect('o.totalPrice', 'total') .innerJoin('o.user', 'u') From e444029f07fd2bf67b7697991c8c0cfd823a09cd Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 12:26:55 -0400 Subject: [PATCH 71/75] Fix filter lot by branch id --- src/products/services/lot.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/products/services/lot.service.ts b/src/products/services/lot.service.ts index c7d6e17..89b2420 100644 --- a/src/products/services/lot.service.ts +++ b/src/products/services/lot.service.ts @@ -28,11 +28,11 @@ export class LotService { .innerJoinAndSelect('product.manufacturer', 'manufacturer'); if (branchId) { - query.andWhere('lot.branchId = :branchId', { branchId }); + query.andWhere('branch.id = :branchId', { branchId }); } if (productPresentationId) { - query.andWhere('lot.productPresentationId = :productPresentationId', { + query.andWhere('productPresentation.id = :productPresentationId', { productPresentationId, }); } From e967481f091cfbe4733ab6c6335e4075fae2ba25 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 12:39:24 -0400 Subject: [PATCH 72/75] Create inventory movement for manual update of stock --- src/inventory/inventory.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/inventory/inventory.service.ts b/src/inventory/inventory.service.ts index 43da98c..3a7cb8b 100644 --- a/src/inventory/inventory.service.ts +++ b/src/inventory/inventory.service.ts @@ -119,7 +119,17 @@ export class InventoryService { }; } const inventory = await this.inventoryRepository.findOne(where); + if (!inventory) { + throw new NotFoundException(`Inventory #${id} not found`); + } + const delta = updateInventoryDTO.stockQuantity! - inventory.stockQuantity; + const movement = this.inventoryMovementRepository.create({ + inventory, + quantity: Math.abs(delta), + type: delta > 0 ? MovementType.IN : MovementType.OUT, + }); const updatedInventory = { ...inventory, ...updateInventoryDTO }; + await this.inventoryMovementRepository.save(movement); return await this.inventoryRepository.save(updatedInventory); } From 31630a3f69fd5fa953d3a3a921b0f3ce9ce4a451 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 13:53:31 -0400 Subject: [PATCH 73/75] Generate random password --- src/user/user.service.ts | 11 ++++++++--- src/utils/password.ts | 10 ++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 src/utils/password.ts diff --git a/src/user/user.service.ts b/src/user/user.service.ts index b24d5c2..bd2295b 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -18,6 +18,7 @@ import { UserMoto } from './entities/user-moto.entity'; import { UpdateUserMotoDTO } from './dto/user-moto.dto'; import { BranchService } from 'src/branch/branch.service'; import { EmailService } from 'src/email/email.service'; +import { generateRandomPassword } from 'src/utils/password'; @Injectable() export class UserService { @@ -127,7 +128,7 @@ export class UserService { if (documentUsed) { throw new BadRequestException('The document is already in use'); } - const password: string = this.configService.get('ADMIN_PASSWORD', ''); + const password: string = generateRandomPassword(8); const hashedPassword = await bcrypt.hash(password, 10); const newUser = this.userRepository.create(user); newUser.password = hashedPassword; @@ -149,8 +150,12 @@ export class UserService { await this.emailService.sendEmail({ recipients: [{ email: user.email, name: user.firstName }], subject: 'Welcome to Pharmatech', - html: `

Hi, your account has been created. Your password is: ${password}

`, - text: `Hi, your account has been created. Your password is: ${password}`, + html: ` +

Hola ${newUser.firstName}, tu cuenta ha sido creada.

+

Aca esta tu contraseña: ${password}

+

Por favor, cambiala al iniciar sesión.

+ `, + text: `Hola ${newUser.firstName}, tu cuenta ha sido creada, aca esta tu contraseña: ${password}`, }); const profile = new Profile(); diff --git a/src/utils/password.ts b/src/utils/password.ts new file mode 100644 index 0000000..3c08951 --- /dev/null +++ b/src/utils/password.ts @@ -0,0 +1,10 @@ +export function generateRandomPassword(length: number = 8): string { + const charset = + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+[]{}|;:,.<>?'; + let password = ''; + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * charset.length); + password += charset[randomIndex]; + } + return password; +} From 9ffd2ec6d9b4e53d38891aee8442e520fec4197f Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 14:12:06 -0400 Subject: [PATCH 74/75] Seed default admin user --- .env.example | 3 +- package.json | 1 + ...8541346159-seed-default-admin-migration.ts | 49 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/user/migrations/1748541346159-seed-default-admin-migration.ts diff --git a/.env.example b/.env.example index 270aabb..4024433 100644 --- a/.env.example +++ b/.env.example @@ -29,4 +29,5 @@ PHARMATECH_SENDER="Andy de Pharmatech " # Resemd RESEND_API_KEY= -ADMIN_PASSWORD= \ No newline at end of file +ADMIN_PASSWORD= +ADMIN_EMAIL= \ No newline at end of file diff --git a/package.json b/package.json index 7b32eb4..3faea80 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "test:watch": "vitest run --watch --config vitest.config.ts", "test:coverage": "vitest run --coverage.enabled --coverage.all --config vitest.config.ts", "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", + "migration:create": "npm run typeorm -- migration:create", "migration:generate": "npm run typeorm -- migration:generate -d ./src/database/typeorm.config.ts", "migration:run": "npm run typeorm -- migration:run -d ./src/database/typeorm.config.ts", "migration:revert": "npm run typeorm -- migration:revert -d ./src/database/typeorm.config.ts", diff --git a/src/user/migrations/1748541346159-seed-default-admin-migration.ts b/src/user/migrations/1748541346159-seed-default-admin-migration.ts new file mode 100644 index 0000000..b55d93d --- /dev/null +++ b/src/user/migrations/1748541346159-seed-default-admin-migration.ts @@ -0,0 +1,49 @@ +import * as bcrypt from 'bcryptjs'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SeedDefaultAdminMigration1748541346159 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + const adminEmail = process.env.ADMIN_EMAIL; + const adminPassword = process.env.ADMIN_PASSWORD; + if (!adminEmail || !adminPassword) { + throw new Error( + 'ADMIN_EMAIL and ADMIN_PASSWORD environment variables must be set', + ); + } + const hashedPassword = await bcrypt.hash(adminPassword, 10); + const id = '88e4e6de-77fb-49d4-8ec2-d925e3a29a55'; + await queryRunner.query(` + INSERT INTO public.user (id, email, password, role, first_name, last_name, document_id, is_validated) + VALUES ( + '${id}', + '${adminEmail}', + '${hashedPassword}', + 'admin', + 'Administrador', + 'Pharmatech', + '1', + true + ); + INSERT INTO public.profile (user_id, birth_date) + VALUES ( + '${id}', + '2000-01-01' + ); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + const adminEmail = process.env.ADMIN_EMAIL; + if (!adminEmail) { + throw new Error('ADMIN_EMAIL environment variable must be set'); + } + await queryRunner.query(` + DELETE FROM public.user WHERE email = '${adminEmail}'; + DELETE FROM public.profile WHERE user_id = ( + SELECT id FROM public.user WHERE email = '${adminEmail}' + ); + `); + } +} From 782b62b68dcb371b0cf2c2d6b1a2f391053534a2 Mon Sep 17 00:00:00 2001 From: Andres Alvarez Date: Thu, 29 May 2025 15:38:49 -0400 Subject: [PATCH 75/75] Upgrade version to 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3faea80..3907d1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "api", - "version": "0.5.0", + "version": "1.0.0", "description": "", "author": "", "private": true,