From e242730cbafdc83eedabd58a31aaec759ac8cb4d Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 7 Jul 2026 18:56:57 +0330 Subject: [PATCH] up --- src/modules/invoice/invoice.service.ts | 17 +++++- .../product/controllers/product.controller.ts | 25 +++++++- .../product/providers/product.service.ts | 58 +++++++++++++++++++ .../repositories/category.repository.ts | 37 +++++++++--- .../repositories/product.repository.ts | 2 +- src/seeders/data/admins.data.ts | 12 ++-- 6 files changed, 133 insertions(+), 18 deletions(-) diff --git a/src/modules/invoice/invoice.service.ts b/src/modules/invoice/invoice.service.ts index 5dd87c8..2568fdf 100644 --- a/src/modules/invoice/invoice.service.ts +++ b/src/modules/invoice/invoice.service.ts @@ -112,6 +112,7 @@ export class InvoiceService { // let subTotal = 0; + for (const item of dto.items) { const product = await this.productService.findOneOrFail(item.productId); const { discount, discountPercent } = this.resolveItemDiscount( @@ -131,7 +132,7 @@ export class InvoiceService { }); invoice.items.add(invoiceItem); } - + if (dto.requestId) { await this.requestService.hardDeleteRequestAndItems(dto.requestId, em); @@ -140,13 +141,25 @@ export class InvoiceService { await em.flush(); return invoice; }); + + const allItems = invoice.items.getItems(); + let subTotal = 0; + let discount = 0; + for (const item of allItems) { + subTotal += item.unitPrice * item.quantity; + discount += item.discount ?? 0; + } + const taxableBase = Math.max(0, subTotal - discount); + const taxAmount = enableTax ? Math.round(taxableBase * TAX_RATE) : 0; + const total = taxableBase + taxAmount; + // TODO : maybe it is needed to get a fresh instance. this.eventEmitter.emit( InvoiceCreatedEvent.name, new InvoiceCreatedEvent( invoice.id, String(invoice.invoiceNumber), - invoice.total!, + total, user.id, ), ); diff --git a/src/modules/product/controllers/product.controller.ts b/src/modules/product/controllers/product.controller.ts index 996a175..f3a28d4 100644 --- a/src/modules/product/controllers/product.controller.ts +++ b/src/modules/product/controllers/product.controller.ts @@ -85,11 +85,23 @@ export class ProductController { /* =============================== PRODUCT ================================= */ + @Get('public/categories') + @UseGuards(AuthGuard) + @ApiOperation({ summary: 'Get list of categories' }) + async findAllCategoriesAsUser() { + return this.categoryService.findAll({ limit: 1000, isActive: true }); + } + @Get('public/products') @UseGuards(AuthGuard) @ApiOperation({ summary: 'Get list of products' }) - async findAllAsUser() { - const result = await this.productService.findAll({ limit: 1000 }); + @ApiQuery({ name: 'categoryId', required: false, type: String }) + async findAllAsUser(@Query() dto: FindproductsDto) { + const result = await this.productService.findAll({ + limit: 1000, + isActive: true, + categoryId: dto.categoryId, + }); return result; } @@ -144,5 +156,14 @@ export class ProductController { return this.productService.remove(id); } + @Post('admin/products/:id/duplicate') + @UseGuards(AdminAuthGuard) + @Permissions(PermissionEnum.MANAGE_PRODUCTS) + @ApiOperation({ summary: 'Duplicate a product with all fields' }) + @ApiParam({ name: 'id', required: true }) + duplicate(@Param('id') id: string) { + return this.productService.duplicate(id); + } + } diff --git a/src/modules/product/providers/product.service.ts b/src/modules/product/providers/product.service.ts index dfcac28..38b3bc0 100644 --- a/src/modules/product/providers/product.service.ts +++ b/src/modules/product/providers/product.service.ts @@ -7,6 +7,10 @@ import { RequiredEntityData } from '@mikro-orm/core'; import { Product } from '../entities/product.entity'; import { CategoryMessage, productMessage } from 'src/common/enums/message.enum'; import { CategoryRepository } from '../repositories/category.repository'; +import { FieldRepository } from 'src/modules/form-builder/repository/field.repository'; +import { FieldOption } from 'src/modules/form-builder/entities/field-option.entity'; +import { Field } from 'src/modules/form-builder/entities/field.entity'; +import { EntityType } from 'src/modules/form-builder/interface/print'; @Injectable() export class ProductService { @@ -14,6 +18,7 @@ export class ProductService { constructor( private readonly productRepository: ProductRepository, private readonly categoryRepository: CategoryRepository, + private readonly fieldRepository: FieldRepository, private readonly em: EntityManager, ) { } @@ -103,4 +108,57 @@ export class ProductService { if (!product) throw new NotFoundException(productMessage.NOT_FOUND); return product; } + + async duplicate(productId: string) { + const product = await this.productRepository.findOne( + { id: productId }, + { populate: ['category'] }, + ); + if (!product) { + throw new NotFoundException(productMessage.NOT_FOUND); + } + + const fields = await this.fieldRepository.find( + { entityId: productId, entityType: EntityType.product }, + { populate: ['options'], orderBy: { order: 'ASC' } }, + ); + + return this.em.transactional(async (em) => { + const newProduct = em.create(Product, { + category: product.category, + title: `${product.title} - کپی`, + desc: product.desc, + linkUrl: product.linkUrl, + isActive: product.isActive, + images: product.images ? [...product.images] : undefined, + order: product.order, + }); + em.persist(newProduct); + + for (const field of fields) { + const newField = em.create(Field, { + entityType: EntityType.product, + entityId: newProduct.id, + name: field.name, + type: field.type, + isRequired: field.isRequired, + multiple: field.multiple, + order: field.order, + }); + em.persist(newField); + + for (const option of field.options.getItems()) { + const newOption = em.create(FieldOption, { + value: option.value, + order: option.order, + field: newField, + }); + em.persist(newOption); + } + } + + await em.flush(); + return newProduct; + }); + } } diff --git a/src/modules/product/repositories/category.repository.ts b/src/modules/product/repositories/category.repository.ts index f126213..d33afb7 100644 --- a/src/modules/product/repositories/category.repository.ts +++ b/src/modules/product/repositories/category.repository.ts @@ -20,12 +20,36 @@ export class CategoryRepository extends EntityRepository { super(em, Category); } + private buildCategoryTree(categories: Category[]): Category[] { + const roots: Category[] = []; + + for (const category of categories) { + category.children.removeAll(); + } + + for (const category of categories) { + const parentId = category.parent?.id; + + if (parentId) { + const parent = categories.find((item) => item.id === parentId); + if (parent) { + parent.children.add(category); + continue; + } + } + + roots.push(category); + } + + return roots; + } + async findAllPaginated(opts: FindCategoriesOpts = {}): Promise> { const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc', isActive } = opts; const offset = (page - 1) * limit; - const where: FilterQuery = { parent: null }; + const where: FilterQuery = {}; if (typeof isActive === 'boolean') { where.isActive = isActive; @@ -37,13 +61,14 @@ export class CategoryRepository extends EntityRepository { ] } - const [data, total] = await this.findAndCount(where, { - limit, - offset, + const allCategories = await this.find(where, { orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - populate: ['children'] + populate: ['parent'], }); + const roots = this.buildCategoryTree(allCategories); + const data = roots.slice(offset, offset + limit); + const total = roots.length; const totalPages = Math.ceil(total / limit); return { @@ -55,7 +80,5 @@ export class CategoryRepository extends EntityRepository { totalPages, }, }; - - } } diff --git a/src/modules/product/repositories/product.repository.ts b/src/modules/product/repositories/product.repository.ts index 5f4f9f5..a63de88 100644 --- a/src/modules/product/repositories/product.repository.ts +++ b/src/modules/product/repositories/product.repository.ts @@ -45,7 +45,7 @@ export class ProductRepository extends EntityRepository { limit, offset, orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - populate: [], + populate: ['category'], }); const totalPages = Math.ceil(total / limit); diff --git a/src/seeders/data/admins.data.ts b/src/seeders/data/admins.data.ts index 11e7ecc..b5e6f19 100644 --- a/src/seeders/data/admins.data.ts +++ b/src/seeders/data/admins.data.ts @@ -13,13 +13,13 @@ export const adminsData: AdminData[] = [ roleName: 'admin', }, - // { - // phone: '09121724095', - // firstName: 'آقای', - // lastName: 'نواعتقاد', - // roleName: 'admin', + { + phone: '09121724095', + firstName: 'آقای', + lastName: 'نواعتقاد', + roleName: 'admin', - // }, + }, // { // phone: '09185290775', // firstName: 'حمید',