From 2e6c8b7acb5b4779cdb4c210ebd36383291b41bd Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Sun, 27 Apr 2025 18:10:07 -0400 Subject: [PATCH 1/2] Add Validations --- src/inventory/inventory.module.ts | 1 + src/order/dto/order.ts | 9 +++ src/order/order.module.ts | 8 ++- src/order/order.service.ts | 60 +++++++++++++++++-- src/products/products.module.ts | 2 + .../services/product-presentation.service.ts | 38 +++++++++++- 6 files changed, 111 insertions(+), 7 deletions(-) diff --git a/src/inventory/inventory.module.ts b/src/inventory/inventory.module.ts index 2137976..0954aef 100644 --- a/src/inventory/inventory.module.ts +++ b/src/inventory/inventory.module.ts @@ -36,5 +36,6 @@ import { InventorySubscriber } from '../inventory/subscribers/inventory.subscrib CountryService, InventorySubscriber, ], + exports: [InventoryService, TypeOrmModule], }) export class InventoryModule {} diff --git a/src/order/dto/order.ts b/src/order/dto/order.ts index f554d47..fcfe5ae 100644 --- a/src/order/dto/order.ts +++ b/src/order/dto/order.ts @@ -52,6 +52,15 @@ export class CreateOrderDTO { @IsOptional() userAddressId?: string; + @ApiProperty({ + description: 'Código de cupón (opcional)', + example: 'SAVE10B', + required: false, + }) + @IsOptional() + @IsString() + couponCode?: string; + @ApiProperty({ description: 'List of products in the order', type: [CreateOrderDetailDTO], diff --git a/src/order/order.module.ts b/src/order/order.module.ts index 68a2edd..3b9894f 100644 --- a/src/order/order.module.ts +++ b/src/order/order.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { OrderService } from './order.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Order, OrderDetail } from './entities/order.entity'; @@ -21,6 +21,9 @@ import { } from './entities/order_delivery.entity'; import { OrderDeliveryController } from './controllers/order-delivery.controller'; import { OrderController } from './controllers/order.controller'; +import { Coupon } from 'src/discount/entities/coupon.entity'; +import { CouponService } from 'src/discount/services/coupon.service'; +import { InventoryModule } from 'src/inventory/inventory.module'; @Module({ imports: [ @@ -35,8 +38,10 @@ import { OrderController } from './controllers/order.controller'; Promo, OrderDelivery, OrderDetailDelivery, + Coupon, ]), AuthModule, + forwardRef(() => InventoryModule), ], controllers: [OrderController, OrderDeliveryController], providers: [ @@ -47,6 +52,7 @@ import { OrderController } from './controllers/order.controller'; StateService, CountryService, PromoService, + CouponService, ], }) export class OrderModule {} diff --git a/src/order/order.service.ts b/src/order/order.service.ts index cfae7b8..ae3b552 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -23,6 +23,7 @@ import { import { UpdateDeliveryDTO } from './dto/order-delivery.dto'; import { UserAddress } from 'src/user/entities/user-address.entity'; import { UserService } from 'src/user/user.service'; +import { CouponService } from 'src/discount/services/coupon.service'; @Injectable() export class OrderService { @@ -36,6 +37,7 @@ export class OrderService { @InjectRepository(OrderDelivery) private orderDeliveryRepository: Repository, private userService: UserService, + private couponService: CouponService, ) {} async create(user: User, createOrderDTO: CreateOrderDTO) { @@ -78,7 +80,17 @@ export class OrderService { ...product, quantity: productsById[product.id], })); - const totalPrice = productsWithQuantity.reduce((acc, product) => { + for (const item of productsWithQuantity) { + const available = await this.productPresentationService.getTotalInventory( + item.id, + ); + if (item.quantity > available) { + throw new BadRequestException( + `Insufficient inventory for productPresentation ${item.id}`, + ); + } + } + let totalPrice = productsWithQuantity.reduce((acc, product) => { const price = product.promo ? product.price - (product.price * product.promo.discount) / 100 : product.price; @@ -88,7 +100,12 @@ export class OrderService { if (productsWithQuantity.length == 0) { throw new BadRequestException('No products found'); } - + if (createOrderDTO.couponCode) { + totalPrice = await this.validateAndApplyCoupon( + createOrderDTO.couponCode, + totalPrice, + ); + } const orderToCreate = this.orderRepository.create({ user, branch, @@ -121,7 +138,30 @@ export class OrderService { } return order; } + private async validateAndApplyCoupon( + couponCode: string, + totalPrice: number, + ): Promise { + const coupon = await this.couponService.findOne(couponCode); + if (coupon.expirationDate.getTime() < Date.now()) { + throw new BadRequestException('Coupon expired'); + } + if (totalPrice < coupon.minPurchase) { + throw new BadRequestException( + `Minimum purchase of ${coupon.minPurchase} required to use this coupon`, + ); + } + if (coupon.maxUses <= 0) { + throw new BadRequestException('Coupon has no remaining uses'); + } + const discountAmount = Math.round((totalPrice * coupon.discount) / 100); + const newTotal = totalPrice - discountAmount; + await this.couponService.update(coupon.code, { + maxUses: coupon.maxUses - 1, + }); + return newTotal; + } async findAll( page: number, pageSize: number, @@ -163,11 +203,21 @@ export class OrderService { } return order; } - async update(id: string, status: OrderStatus) { + async update(id: string, status: OrderStatus): Promise { const order = await this.findOne(id); - if (!order) { - throw new BadRequestException('Order not found'); + if (order.status === OrderStatus.COMPLETED) { + throw new BadRequestException('A COMPLETED order cannot be modified'); } + if (status === OrderStatus.APPROVED) { + for (const detail of order.details) { + await this.productPresentationService.decrementInventory( + detail.productPresentation.id, + order.branch.id, + detail.quantity, + ); + } + } + order.status = status; return await this.orderRepository.save(order); } diff --git a/src/products/products.module.ts b/src/products/products.module.ts index 4526f3c..29011dc 100644 --- a/src/products/products.module.ts +++ b/src/products/products.module.ts @@ -26,6 +26,7 @@ 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 { InventoryModule } from 'src/inventory/inventory.module'; @Module({ imports: [ @@ -40,6 +41,7 @@ import { RecommendationService } from 'src/recommendation/recommendation.service ]), AuthModule, forwardRef(() => DiscountModule), + forwardRef(() => InventoryModule), ], controllers: [ ProductsController, diff --git a/src/products/services/product-presentation.service.ts b/src/products/services/product-presentation.service.ts index 1921bee..c2d84f3 100644 --- a/src/products/services/product-presentation.service.ts +++ b/src/products/services/product-presentation.service.ts @@ -3,10 +3,15 @@ import { ProductPresentation } from '../entities/product-presentation.entity'; import { CreateProductPresentationDTO } from '../dto/product-presentation.dto'; import { Product } from '../entities/product.entity'; import { Presentation } from '../entities/presentation.entity'; -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + BadRequestException, +} from '@nestjs/common'; import { In, IsNull, Not, Repository } from 'typeorm'; import { UpdateProductPresentationDTO } from '../dto/product-presentation.dto'; import { PromoService } from '../../discount/services/promo.service'; +import { Inventory } from '../../inventory/entities/inventory.entity'; @Injectable() export class ProductPresentationService { @@ -14,6 +19,8 @@ export class ProductPresentationService { @InjectRepository(ProductPresentation) private readonly repository: Repository, private readonly promoService: PromoService, + @InjectRepository(Inventory) + private readonly inventoryRepository: Repository, ) {} async findOne(id: string): Promise { @@ -135,4 +142,33 @@ export class ProductPresentationService { relations: ['product', 'presentation', 'promo', 'inventories'], }); } + async getTotalInventory(presentationId: string): Promise { + const rawResult = await this.inventoryRepository + .createQueryBuilder('inv') + .select('SUM(inv.stock_quantity)', 'sum') + .where('inv.product_presentation_id = :id', { id: presentationId }) + .getRawOne<{ sum: string }>(); + const total = rawResult?.sum ?? '0'; + return Number(total); + } + + async decrementInventory( + presentationId: string, + branchId: string, + amount: number, + ): Promise { + const inv = await this.inventoryRepository.findOne({ + where: { + productPresentation: { id: presentationId }, + branch: { id: branchId }, + }, + }); + if (!inv || inv.stockQuantity < amount) { + throw new BadRequestException( + 'Insufficient branch inventory to approve order', + ); + } + inv.stockQuantity -= amount; + await this.inventoryRepository.save(inv); + } } From a2ee702428cfba4bc5c2dc1b34adec04770d8921 Mon Sep 17 00:00:00 2001 From: Dazzlin00 Date: Sun, 27 Apr 2025 21:31:07 -0400 Subject: [PATCH 2/2] fix --- src/inventory/inventory.service.ts | 45 ++++++++++++++++++- src/order/order.service.ts | 11 +++-- src/products/products.module.ts | 2 - .../services/product-presentation.service.ts | 38 +--------------- 4 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/inventory/inventory.service.ts b/src/inventory/inventory.service.ts index 2a9e877..4ffece4 100644 --- a/src/inventory/inventory.service.ts +++ b/src/inventory/inventory.service.ts @@ -1,4 +1,8 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + BadRequestException, +} from '@nestjs/common'; import { CreateInventoryDTO, UpdateInventoryDTO, @@ -136,7 +140,6 @@ export class InventoryService { return this.inventoryRepository.save(toUpdate); } - async updateBulkByBranch( branchId: string, bulkUpdateDto: BulkUpdateInventoryDTO, @@ -146,4 +149,42 @@ export class InventoryService { const inventoryMap = this.buildInventoryMap(inventories); return this.applyBulkUpdate(branchId, bulkUpdateDto, inventoryMap); } + async getBulkTotalInventory( + presentationIds: string[], + ): Promise> { + const rows = await this.inventoryRepository + .createQueryBuilder('inv') + .select('inv.product_presentation_id', 'presentationId') + .addSelect('SUM(inv.stock_quantity)', 'totalStock') + .where('inv.product_presentation_id IN (:...ids)', { + ids: presentationIds, + }) + .groupBy('inv.product_presentation_id') + .getRawMany<{ presentationId: string; totalStock: string }>(); + + const inventoryMap: Record = {}; + for (const { presentationId, totalStock } of rows) { + inventoryMap[presentationId] = Number(totalStock); + } + return inventoryMap; + } + async decrementInventory( + presentationId: string, + branchId: string, + amount: number, + ): Promise { + const inv = await this.inventoryRepository.findOne({ + where: { + productPresentation: { id: presentationId }, + branch: { id: branchId }, + }, + }); + if (!inv || inv.stockQuantity < amount) { + throw new BadRequestException( + 'Insufficient branch inventory to approve order', + ); + } + inv.stockQuantity -= amount; + await this.inventoryRepository.save(inv); + } } diff --git a/src/order/order.service.ts b/src/order/order.service.ts index ae3b552..e166032 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -24,6 +24,7 @@ import { UpdateDeliveryDTO } from './dto/order-delivery.dto'; import { UserAddress } from 'src/user/entities/user-address.entity'; import { UserService } from 'src/user/user.service'; import { CouponService } from 'src/discount/services/coupon.service'; +import { InventoryService } from 'src/inventory/inventory.service'; @Injectable() export class OrderService { @@ -37,6 +38,7 @@ export class OrderService { @InjectRepository(OrderDelivery) private orderDeliveryRepository: Repository, private userService: UserService, + private readonly inventoryService: InventoryService, private couponService: CouponService, ) {} @@ -80,10 +82,11 @@ export class OrderService { ...product, quantity: productsById[product.id], })); + const presentationIds = productsWithQuantity.map((p) => p.id); + const inventories = + await this.inventoryService.getBulkTotalInventory(presentationIds); for (const item of productsWithQuantity) { - const available = await this.productPresentationService.getTotalInventory( - item.id, - ); + const available = inventories[item.id] ?? 0; if (item.quantity > available) { throw new BadRequestException( `Insufficient inventory for productPresentation ${item.id}`, @@ -210,7 +213,7 @@ export class OrderService { } if (status === OrderStatus.APPROVED) { for (const detail of order.details) { - await this.productPresentationService.decrementInventory( + await this.inventoryService.decrementInventory( detail.productPresentation.id, order.branch.id, detail.quantity, diff --git a/src/products/products.module.ts b/src/products/products.module.ts index 29011dc..4526f3c 100644 --- a/src/products/products.module.ts +++ b/src/products/products.module.ts @@ -26,7 +26,6 @@ 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 { InventoryModule } from 'src/inventory/inventory.module'; @Module({ imports: [ @@ -41,7 +40,6 @@ import { InventoryModule } from 'src/inventory/inventory.module'; ]), AuthModule, forwardRef(() => DiscountModule), - forwardRef(() => InventoryModule), ], controllers: [ ProductsController, diff --git a/src/products/services/product-presentation.service.ts b/src/products/services/product-presentation.service.ts index c2d84f3..1921bee 100644 --- a/src/products/services/product-presentation.service.ts +++ b/src/products/services/product-presentation.service.ts @@ -3,15 +3,10 @@ import { ProductPresentation } from '../entities/product-presentation.entity'; import { CreateProductPresentationDTO } from '../dto/product-presentation.dto'; import { Product } from '../entities/product.entity'; import { Presentation } from '../entities/presentation.entity'; -import { - Injectable, - NotFoundException, - BadRequestException, -} from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { In, IsNull, Not, Repository } from 'typeorm'; import { UpdateProductPresentationDTO } from '../dto/product-presentation.dto'; import { PromoService } from '../../discount/services/promo.service'; -import { Inventory } from '../../inventory/entities/inventory.entity'; @Injectable() export class ProductPresentationService { @@ -19,8 +14,6 @@ export class ProductPresentationService { @InjectRepository(ProductPresentation) private readonly repository: Repository, private readonly promoService: PromoService, - @InjectRepository(Inventory) - private readonly inventoryRepository: Repository, ) {} async findOne(id: string): Promise { @@ -142,33 +135,4 @@ export class ProductPresentationService { relations: ['product', 'presentation', 'promo', 'inventories'], }); } - async getTotalInventory(presentationId: string): Promise { - const rawResult = await this.inventoryRepository - .createQueryBuilder('inv') - .select('SUM(inv.stock_quantity)', 'sum') - .where('inv.product_presentation_id = :id', { id: presentationId }) - .getRawOne<{ sum: string }>(); - const total = rawResult?.sum ?? '0'; - return Number(total); - } - - async decrementInventory( - presentationId: string, - branchId: string, - amount: number, - ): Promise { - const inv = await this.inventoryRepository.findOne({ - where: { - productPresentation: { id: presentationId }, - branch: { id: branchId }, - }, - }); - if (!inv || inv.stockQuantity < amount) { - throw new BadRequestException( - 'Insufficient branch inventory to approve order', - ); - } - inv.stockQuantity -= amount; - await this.inventoryRepository.save(inv); - } }