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/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/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..e166032 100644 --- a/src/order/order.service.ts +++ b/src/order/order.service.ts @@ -23,6 +23,8 @@ 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'; +import { InventoryService } from 'src/inventory/inventory.service'; @Injectable() export class OrderService { @@ -36,6 +38,8 @@ export class OrderService { @InjectRepository(OrderDelivery) private orderDeliveryRepository: Repository, private userService: UserService, + private readonly inventoryService: InventoryService, + private couponService: CouponService, ) {} async create(user: User, createOrderDTO: CreateOrderDTO) { @@ -78,7 +82,18 @@ export class OrderService { ...product, quantity: productsById[product.id], })); - const totalPrice = productsWithQuantity.reduce((acc, product) => { + const presentationIds = productsWithQuantity.map((p) => p.id); + const inventories = + await this.inventoryService.getBulkTotalInventory(presentationIds); + for (const item of productsWithQuantity) { + const available = inventories[item.id] ?? 0; + 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 +103,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 +141,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 +206,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.inventoryService.decrementInventory( + detail.productPresentation.id, + order.branch.id, + detail.quantity, + ); + } + } + order.status = status; return await this.orderRepository.save(order); }