From e2b065aa2bfea77ac523b20953fd840f3bf44a9c Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 27 Jan 2026 14:52:55 +0330 Subject: [PATCH] clean order services --- src/modules/admin/providers/admin.service.ts | 10 +- .../order/controllers/order.controller.ts | 1 - src/modules/order/dto/create-order.dto.ts | 4 +- .../order/entities/order-item.entity.ts | 11 +- .../order/interface/order.interface.ts | 38 +- src/modules/order/providers/order.service.ts | 506 +++++++++--------- .../payment/services/payments.service.ts | 2 +- .../product/providers/product.service.ts | 17 +- src/modules/user/providers/user.service.ts | 7 + 9 files changed, 319 insertions(+), 277 deletions(-) diff --git a/src/modules/admin/providers/admin.service.ts b/src/modules/admin/providers/admin.service.ts index 0e174ee..97f4a4f 100644 --- a/src/modules/admin/providers/admin.service.ts +++ b/src/modules/admin/providers/admin.service.ts @@ -1,4 +1,4 @@ -import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common'; import { Admin } from '../entities/admin.entity'; import { Role } from '../../roles/entities/role.entity'; import { EntityManager, RequiredEntityData } from '@mikro-orm/postgresql'; @@ -113,4 +113,12 @@ export class AdminService { // TODO :L is this correct to soft delete return this.em.removeAndFlush(admin); } + + async findOrFail(adminId: string) { + const admin = await this.findById(adminId) + if (!admin) { + throw new BadRequestException("Admin not found") + } + return admin + } } diff --git a/src/modules/order/controllers/order.controller.ts b/src/modules/order/controllers/order.controller.ts index dbfc0cd..e88ad91 100644 --- a/src/modules/order/controllers/order.controller.ts +++ b/src/modules/order/controllers/order.controller.ts @@ -23,7 +23,6 @@ export class OrderController { @UseGuards(AuthGuard) @ApiOperation({ summary: 'create order ' }) createOrder(@UserId() userId: string, @Body() body: CreateOrderDto) { - console.log(userId) return this.orderService.createOrderAsUser(userId, body); } diff --git a/src/modules/order/dto/create-order.dto.ts b/src/modules/order/dto/create-order.dto.ts index 2e96169..9302118 100644 --- a/src/modules/order/dto/create-order.dto.ts +++ b/src/modules/order/dto/create-order.dto.ts @@ -15,7 +15,7 @@ export class CreateOrderItemDtoAsAdmin { @IsInt() @ApiProperty() - productId: bigint; + productId: number; @ApiProperty() @IsNumber() @@ -76,7 +76,7 @@ export class CreateOrderItemDto { @IsInt() @ApiProperty() - productId: bigint; + productId: number; @ApiProperty() @IsNumber() diff --git a/src/modules/order/entities/order-item.entity.ts b/src/modules/order/entities/order-item.entity.ts index 197c982..547b272 100644 --- a/src/modules/order/entities/order-item.entity.ts +++ b/src/modules/order/entities/order-item.entity.ts @@ -1,4 +1,4 @@ -import { Entity, Formula, Index, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; +import { Entity, Formula, Index, ManyToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Order } from './order.entity'; import { Product } from 'src/modules/product/entities/product.entity'; @@ -7,7 +7,8 @@ import { IAttribute } from '../interface/order.interface'; @Entity({ tableName: 'order_items' }) @Index({ properties: ['order'] }) -export class OrderItem extends BaseEntity { +export class OrderItem { + [OptionalProps]?: 'createdAt' | 'deletedAt' | 'subTotal' | 'total'; @PrimaryKey({ type: 'bigint', autoincrement: true }) id: bigint @@ -52,4 +53,10 @@ export class OrderItem extends BaseEntity { @Property({ nullable: true, columnType: 'timestamptz' }) confirmedAt?: Date; + + @Property({ defaultRaw: 'now()', columnType: 'timestamptz' }) + createdAt: Date = new Date(); + + @Property({ nullable: true, columnType: 'timestamptz' }) + deletedAt?: Date; } diff --git a/src/modules/order/interface/order.interface.ts b/src/modules/order/interface/order.interface.ts index cd123c4..5afe004 100644 --- a/src/modules/order/interface/order.interface.ts +++ b/src/modules/order/interface/order.interface.ts @@ -1,3 +1,6 @@ +import { Product } from "src/modules/product/entities/product.entity" +import { Order } from "../entities/order.entity" + export enum OrderStatusEnum { CREATED = 'created', @@ -26,21 +29,27 @@ export enum OrderStatusEnum { } -export interface IAddOrderItem { - productId: bigint; +export interface CreateOrderItemParam{ quantity: number - attributes: IAttribute[] - description: string - attachments: { url: string, type: string }[] + attributes?: IAttribute[] + description?: string + attachments?: { url: string, type: string }[] unitPrice?: number discount?: number } -export type IUpdateOrderItem = Partial -export interface ICreateOrder { +export interface CreateOrderParam { userId: string - items: IAddOrderItem[]; + items: Array<{ + productId: number, + quantity: number + attributes: IAttribute[] + description: string + attachments: { url: string, type: string }[] + unitPrice?: number + discount?: number + }>; status: OrderStatusEnum //optional enableTax?: Boolean @@ -51,6 +60,17 @@ export interface ICreateOrder { designerId?: string } -export type IUpdateOrder = Partial> +export type UpdateOrderParam = Partial> + + +export interface UpdateOrderItem { + productId?: number, + quantity?: number + attributes?: IAttribute[] + description?: string + attachments?: { url: string, type: string }[] + unitPrice?: number + discount?: number +} export interface IAttribute { attributeId: number, value: string | number | boolean } \ No newline at end of file diff --git a/src/modules/order/providers/order.service.ts b/src/modules/order/providers/order.service.ts index 468bbee..aa0fa53 100644 --- a/src/modules/order/providers/order.service.ts +++ b/src/modules/order/providers/order.service.ts @@ -1,15 +1,18 @@ import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { EntityManager } from '@mikro-orm/postgresql'; -import { OrderItem } from '../entities/order-item.entity'; import { OrderRepository } from '../repositories/order.repository'; import { FindOrdersDto } from '../dto/find-orders.dto'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { CreateOrderDto, CreateOrderItemAsUserDto, - CreateOrderItemDtoAsAdmin, CreateOrderAsAdminDto + CreateOrderItemDtoAsAdmin } from '../dto/create-order.dto'; import { UserService } from 'src/modules/user/providers/user.service'; -import { IAddOrderItem, ICreateOrder, OrderStatusEnum, IUpdateOrderItem, IUpdateOrder } from '../interface/order.interface'; +import { + CreateOrderItemParam, + CreateOrderParam, OrderStatusEnum, + UpdateOrderItem, UpdateOrderParam +} from '../interface/order.interface'; import { OrderItemRepository } from '../repositories/order-item.repository'; import { ProductService } from 'src/modules/product/providers/product.service'; import { ProductRepository } from 'src/modules/product/repositories/product.repository'; @@ -20,6 +23,9 @@ import { Order } from '../entities/order.entity'; import { PaymentRepository } from 'src/modules/payment/repositories/payment.repository'; import { UpdateOrderItemDtoAsAdmin, UpdateOrderItemDtoAsUser } from '../dto/update-order-item.dto'; import { Admin } from 'src/modules/admin/entities/admin.entity'; +import { Product } from 'src/modules/product/entities/product.entity'; +import { AdminService } from 'src/modules/admin/providers/admin.service'; +import { OrderItem } from '../entities/order-item.entity'; @Injectable() @@ -31,6 +37,7 @@ export class OrderService { private readonly orderRepository: OrderRepository, private readonly orderItemRepository: OrderItemRepository, private readonly userService: UserService, + private readonly adminService: AdminService, private readonly productService: ProductService, private readonly productRepository: ProductRepository, private readonly ticketRepository: TicketRepository, @@ -40,7 +47,7 @@ export class OrderService { private readonly paymentRepository: PaymentRepository, ) { } - async createOrderAsAdmin(adminId: string, dto: CreateOrderAsAdminDto) { + async createOrderAsAdmin(adminId: string, dto: CreateOrderParam) { const order = await this.createOrder({ ...dto, adminId }) // await this.calculateOrder(order) // this.em.flush() @@ -56,129 +63,11 @@ export class OrderService { } - async createOrder(dto: ICreateOrder) { - const { items, userId, attachments, designerId, enableTax, estimatedDays, paymentMethod, status, adminId } = dto - const user = await this.userService.findById(userId) - if (!user) { - throw new BadRequestException("User not found!") - } + async updateOrderAsAdmin(orderId: string, param: UpdateOrderParam) { + const order = await this.findOrderOrFail(orderId) - let admin: null | Admin = null - if (adminId) { - admin = await this.adminRepository.findOne({ id: adminId }) - if (!admin) { - throw new BadRequestException("Admin not found") - } - } - - let designer: null | Admin = null - if (designerId) { - designer = await this.adminRepository.findOne({ id: designerId }) - if (!designer) { - throw new BadRequestException("designer not found") - } - } - - return this.em.transactional(async (em) => { - - const order = this.orderRepository.create({ - creator: admin, - user, - attachments, - designer, - enableTax, - estimatedDays, - paymentMethod, - status, - }) - - em.persist(order) - - const productIds = items.map(item => item.productId) - - const products = await this.productRepository.find({ - id: { $in: productIds } - }) - if (productIds.length !== products.length) { - throw new BadRequestException("some products not found") - } - - for (const item of items) { - await this.persistOrderItem(order, item) - } - - // TODO : calculation must be done after create order - - // await this.calculateOrder(order) - - await em.flush() - - return order - }) - - - } - - async updateOrder(order: Order, dto: IUpdateOrder) { - const { attachments, adminId, designerId, enableTax, estimatedDays, paymentMethod, status, userId } = dto - - - if (userId) { - const user = await this.userService.findById(userId) - if (!user) { - throw new BadRequestException("User not found!") - } - order.user = user - } - - - if (adminId != undefined) { - const admin = await this.adminRepository.findOne({ id: adminId }) - if (!admin) { - throw new BadRequestException("Admin not found") - } - order.creator = admin - } - - - if (designerId != undefined) { - const designer = await this.adminRepository.findOne({ id: designerId }) - if (!designer) { - throw new BadRequestException("designer not found") - } - order.designer = designer - } - - if (attachments != undefined) { - order.attachments = attachments - } - - if (enableTax != undefined) { - order.enableTax = enableTax - } - - if (estimatedDays != undefined) { - order.estimatedDays = estimatedDays - } - - if (paymentMethod != undefined) { - order.paymentMethod = paymentMethod - } - - if (status != undefined) { - order.status = status - } - - await this.em.persistAndFlush(order) - - return order - } - - async updateOrderAsAdmin(orderId: string, dto: IUpdateOrder) { - const order = await this.findOneOrFail(orderId) - - const updateOrder = await this.updateOrder(order, dto) + const updateOrder = await this.updateOrder(order, param) // const updateOrder = await this.calculateOrder(order) @@ -191,7 +80,7 @@ export class OrderService { async addOrderItemAsUser(userId: string, orderId: string, dto: CreateOrderItemAsUserDto) { - const order = await this.findOneOrFail(orderId) + const order = await this.findOrderOrFail(orderId) if (order.status !== OrderStatusEnum.CREATED) { throw new BadRequestException(`You can not update when status is ${order.status}`) @@ -201,7 +90,9 @@ export class OrderService { throw new BadRequestException(`This order doest belongs to you!`) } - const orderItem = this.persistOrderItem(order, dto) + const product = await this.productService.findOneOrFail(dto.productId) + + const orderItem = this.createOrderItem(order, product, dto) await this.em.flush() @@ -210,9 +101,11 @@ export class OrderService { async addOrderItemAsAdmin(orderId: string, dto: CreateOrderItemDtoAsAdmin) { - const order = await this.findOneOrFail(orderId) + const order = await this.findOrderOrFail(orderId) - const orderItem = this.persistOrderItem(order, dto) + const product = await this.productService.findOneOrFail(dto.productId) + + const orderItem = this.createOrderItem(order, product, dto) await this.em.flush() @@ -223,39 +116,10 @@ export class OrderService { return orderItem } - private async persistOrderItem(order: Order, dto: IAddOrderItem) { - const { attributes, description, productId, quantity, attachments, discount, unitPrice } = dto - - const found = order.items.find(it => it.product.id == productId) - if (found) { - throw new BadRequestException(`Product already exists`) - } - console.log(productId) - const product = await this.productRepository.findOne({ id: productId }) - if (!product) { - throw new BadRequestException(`Product not found`) - } - const orderItem = this.orderItemRepository.create({ - order, - attributes, - description, - quantity, - attachments, - subTotal: 0, - discount: discount ?? 0, - total: 0, - unitPrice: unitPrice ?? 0, - product - }) - - this.em.persist(orderItem) - - return orderItem - } async updateOrderItemAsUser(userId: string, orderId: string, itemId: string, dto: UpdateOrderItemDtoAsUser) { - const order = await this.findOneOrFail(orderId) + const order = await this.findOrderOrFail(orderId) if (order.user.id !== userId) { throw new BadRequestException(`This order doesnt belongs to you`) @@ -272,7 +136,7 @@ export class OrderService { async updateOrderItemAsAdmin(orderId: string, itemId: string, dto: UpdateOrderItemDtoAsAdmin) { - const order = await this.findOneOrFail(orderId) + await this.findOrderOrFail(orderId) const orderItem = await this.updateOrderItem(itemId, dto) @@ -283,57 +147,10 @@ export class OrderService { return orderItem } - async updateOrderItem(itemId: string, dto: IUpdateOrderItem) { - const { attributes, description, productId, quantity, attachments, discount, unitPrice } = dto - - const orderItem = await this.orderItemRepository.findOne({ - id: itemId - }) - - if (!orderItem) { - throw new BadRequestException(`orderItem not found`) - } - - // product is changed - if (productId && orderItem.product.id !== productId) { - const product = await this.productRepository.findOne({ id: productId }) - - if (!product) { - throw new BadRequestException(`product not found`) - } - - orderItem.product = product - } - - if (attributes) { - orderItem.attributes = attributes - } - if (description) { - orderItem.description = description - } - if (quantity) { - orderItem.quantity = quantity - } - - if (attachments) { - orderItem.attachments = attachments - } - if (discount) { - orderItem.discount = discount - } - - if (unitPrice) { - orderItem.unitPrice = unitPrice - } - - await this.em.flush() - - return orderItem - } async removeOrderItemAsAdmin(orderId: string, itemId: string) { - const order = await this.findOneOrFail(orderId) + const order = await this.findOrderOrFail(orderId) await this.removeOrderItem(itemId) @@ -346,7 +163,7 @@ export class OrderService { async removeOrderItemAsUser(userId: string, orderId: string, itemId: string) { - const order = await this.findOneOrFail(orderId) + const order = await this.findOrderOrFail(orderId) if (order.user.id !== userId) { throw new BadRequestException(`this order is not belongs to you`) @@ -361,36 +178,12 @@ export class OrderService { return true } - async removeOrderItem(itemId: string) { - - const orderItem = await this.orderItemRepository.findOne({ - id: itemId - }) - - if (!orderItem) { - throw new BadRequestException(`orderItem not found`) - } - - await this.em.removeAndFlush(orderItem) - - return true - } - async findUserOrders(userId: string, dto: FindOrdersDto) { const orders = await this.orderRepository.findAllPaginated({ userId, ...dto }) return orders } - async findOneOrFail(orderId: string) { - const order = await this.orderRepository.findOne({ id: orderId }, - { populate: ['items', 'items.product', 'payments'] }) - if (!order) { - throw new BadRequestException('Order not found') - } - return order - } - // async calculateOrder(inputOrder?: Order, orderId?: string) { // let order: undefined | Order = inputOrder @@ -471,6 +264,219 @@ export class OrderService { } + + async removeOrderAsUser(orderId: string) { + const order = await this.orderRepository.findOne({ id: orderId }) + if (!order) { + throw new BadRequestException("Order not found") + } + + if (![OrderStatusEnum.CREATED].includes(order.status)) { + throw new BadRequestException("Order can not be deleted") + } + + return this.hardDeleteOrder(orderId) + } + + async updateStatus(orderId: string, newStatus: OrderStatusEnum) { + + const order = await this.findOrderOrFail(orderId) + + order.status = newStatus + + await this.em.flush() + + return order + } + + async createInvoice(orderId: string) { + const order = await this.findOrderOrFail(orderId) + await this.updateStatus(orderId, OrderStatusEnum.INVOICED) + // await this.calculateOrder(order) + await this.em.flush() + } + + // ==================== Entity Methods ==================== + + private async createOrderItem(order: Order, product: Product, dto: CreateOrderItemParam, em?: EntityManager) { + const { attributes, description, quantity, attachments, discount, unitPrice } = dto + const eM = em ?? this.em + return eM.create( + OrderItem, + { + order, + attributes, + description, + quantity, + attachments, + discount: discount ?? undefined, + unitPrice: unitPrice ?? undefined, + product + }) + } + + // ==================== Core Logic ==================== + + async createOrder(dto: CreateOrderParam) { + const { items, userId, attachments, designerId, enableTax, + estimatedDays, paymentMethod, status, adminId } = dto + + // Validate and fetch all required entities + const user = await this.userService.findOrFail(userId) + const admin = adminId ? await this.adminService.findOrFail(adminId) : undefined + const designer = designerId ? await this.adminService.findOrFail(designerId) : undefined + const products = await this.productService.findProductsByIds(items.map(item => item.productId)) + + + return this.em.transactional(async (em) => { + + const order = this.em.create(Order, { + creator: admin, + user, + attachments, + designer, + enableTax, + estimatedDays, + paymentMethod, + status, + }) + + em.persist(order) + + for (const item of items) { + const product = products.find(p => Number(p.id) === item.productId) + + if (!product) { + throw new BadRequestException("Product not found!") + } + + const orderItem = await this.createOrderItem(order, product, item, em) + + order.items.add(orderItem) + } + + // TODO : calculation must be done after create order + + // await this.calculateOrder(order) + + await em.flush() + + return order + }) + + + } + + async updateOrder(order: Order, param: UpdateOrderParam) { + const { attachments, adminId, designerId, enableTax, + estimatedDays, paymentMethod, status, userId } = param + + + if (userId) { + const user = await this.userService.findOrFail(userId) + order.user = user + } + + + if (adminId != undefined) { + const admin = await this.adminService.findOrFail(adminId) + order.creator = admin + } + + + if (designerId != undefined) { + const designer = await this.adminService.findOrFail(designerId) + order.designer = designer + } + + if (attachments != undefined) { + order.attachments = attachments + } + + if (enableTax != undefined) { + order.enableTax = enableTax + } + + if (estimatedDays != undefined) { + order.estimatedDays = estimatedDays + } + + if (paymentMethod != undefined) { + order.paymentMethod = paymentMethod + } + + if (status != undefined) { + order.status = status + } + + await this.em.persistAndFlush(order) + + return order + } + + async updateOrderItem(itemId: string, dto: UpdateOrderItem) { + const { attributes, description, productId, quantity, attachments, discount, unitPrice } = dto + + const orderItem = await this.orderItemRepository.findOne({ + id: itemId + }) + + if (!orderItem) { + throw new BadRequestException(`orderItem not found`) + } + + // product is changed + if (productId && Number(orderItem.product.id) !== productId) { + const product = await this.productRepository.findOne({ id: productId }) + + if (!product) { + throw new BadRequestException(`product not found`) + } + + orderItem.product = product + } + + if (attributes) { + orderItem.attributes = attributes + } + if (description) { + orderItem.description = description + } + if (quantity) { + orderItem.quantity = quantity + } + + if (attachments) { + orderItem.attachments = attachments + } + if (discount) { + orderItem.discount = discount + } + + if (unitPrice) { + orderItem.unitPrice = unitPrice + } + + await this.em.flush() + + return orderItem + } + + async removeOrderItem(itemId: string) { + + const orderItem = await this.orderItemRepository.findOne({ + id: itemId + }) + + if (!orderItem) { + throw new BadRequestException(`orderItem not found`) + } + + await this.em.removeAndFlush(orderItem) + + return true + } + async hardDeleteOrder(orderId: string) { const order = await this.orderRepository.findOne({ id: orderId }) if (!order) { @@ -493,36 +499,16 @@ export class OrderService { } - async removeOrderAsUser(orderId: string) { - const order = await this.orderRepository.findOne({ id: orderId }) + // ==================== Helper Methods ==================== + + async findOrderOrFail(orderId: string) { + const order = await this.orderRepository.findOne({ id: orderId }, + { populate: ['items', 'items.product', 'payments'] }) if (!order) { - throw new BadRequestException("Order not found") + throw new BadRequestException('Order not found') } - - if (![OrderStatusEnum.CREATED].includes(order.status)) { - throw new BadRequestException("Order can not be deleted") - } - - return this.hardDeleteOrder(orderId) - } - - async updateStatus(orderId: string, newStatus: OrderStatusEnum) { - const order = await this.orderRepository.findOne({ id: orderId }) - if (!order) { - throw new BadRequestException("Order not found") - } - - order.status = newStatus - - await this.em.flush() - return order } - async createInvoice(orderId: string) { - const order = await this.findOneOrFail(orderId) - await this.updateStatus(orderId, OrderStatusEnum.INVOICED) - // await this.calculateOrder(order) - await this.em.flush() - } + } diff --git a/src/modules/payment/services/payments.service.ts b/src/modules/payment/services/payments.service.ts index 5a61451..fae9f40 100644 --- a/src/modules/payment/services/payments.service.ts +++ b/src/modules/payment/services/payments.service.ts @@ -61,7 +61,7 @@ export class PaymentService { private async loadAndValidateOrder(orderId: string, dto: PayOrderDto, adminId?: string): Promise { const { amount, method, attachments, description, gateway } = dto - const order = await this.orderService.findOneOrFail(orderId) + const order = await this.orderService.findOrderOrFail(orderId) if (!order) { throw new NotFoundException(OrderMessage.NOT_FOUND); diff --git a/src/modules/product/providers/product.service.ts b/src/modules/product/providers/product.service.ts index 5ddad25..307a7d8 100644 --- a/src/modules/product/providers/product.service.ts +++ b/src/modules/product/providers/product.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { CreateproductDto } from '../dto/create-product.dto'; import { FindproductsDto } from '../dto/find-products.dto'; import { ProductRepository } from '../repositories/product.repository'; @@ -140,4 +140,19 @@ export class ProductService { } + async findProductsByIds(productIds: number[]) { + const products = await this.productRepository.find({ + id: { $in: productIds } + }) + if (productIds.length !== products.length) { + throw new BadRequestException("some products not found") + } + return products + } + + async findOneOrFail(productId: number) { + const product = await this.productRepository.findOne({ id: productId }); + if (!product) throw new NotFoundException(productMessage.NOT_FOUND); + return product; + } } diff --git a/src/modules/user/providers/user.service.ts b/src/modules/user/providers/user.service.ts index 620f6fe..41c243f 100644 --- a/src/modules/user/providers/user.service.ts +++ b/src/modules/user/providers/user.service.ts @@ -68,5 +68,12 @@ export class UserService { return this.userRepository.findAllPaginated(dto) } + async findOrFail(userId: string) { + const user = await this.findById(userId) + if (!user) { + throw new BadRequestException("User not found!") + } + return user + } }