@@ -112,6 +112,7 @@ export class InvoiceService {
|
|||||||
|
|
||||||
// let subTotal = 0;
|
// let subTotal = 0;
|
||||||
|
|
||||||
|
|
||||||
for (const item of dto.items) {
|
for (const item of dto.items) {
|
||||||
const product = await this.productService.findOneOrFail(item.productId);
|
const product = await this.productService.findOneOrFail(item.productId);
|
||||||
const { discount, discountPercent } = this.resolveItemDiscount(
|
const { discount, discountPercent } = this.resolveItemDiscount(
|
||||||
@@ -131,7 +132,7 @@ export class InvoiceService {
|
|||||||
});
|
});
|
||||||
invoice.items.add(invoiceItem);
|
invoice.items.add(invoiceItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (dto.requestId) {
|
if (dto.requestId) {
|
||||||
await this.requestService.hardDeleteRequestAndItems(dto.requestId, em);
|
await this.requestService.hardDeleteRequestAndItems(dto.requestId, em);
|
||||||
@@ -140,13 +141,25 @@ export class InvoiceService {
|
|||||||
await em.flush();
|
await em.flush();
|
||||||
return invoice;
|
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.
|
// TODO : maybe it is needed to get a fresh instance.
|
||||||
this.eventEmitter.emit(
|
this.eventEmitter.emit(
|
||||||
InvoiceCreatedEvent.name,
|
InvoiceCreatedEvent.name,
|
||||||
new InvoiceCreatedEvent(
|
new InvoiceCreatedEvent(
|
||||||
invoice.id,
|
invoice.id,
|
||||||
String(invoice.invoiceNumber),
|
String(invoice.invoiceNumber),
|
||||||
invoice.total!,
|
total,
|
||||||
user.id,
|
user.id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -85,11 +85,23 @@ export class ProductController {
|
|||||||
|
|
||||||
/* =============================== PRODUCT ================================= */
|
/* =============================== 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')
|
@Get('public/products')
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@ApiOperation({ summary: 'Get list of products' })
|
@ApiOperation({ summary: 'Get list of products' })
|
||||||
async findAllAsUser() {
|
@ApiQuery({ name: 'categoryId', required: false, type: String })
|
||||||
const result = await this.productService.findAll({ limit: 1000 });
|
async findAllAsUser(@Query() dto: FindproductsDto) {
|
||||||
|
const result = await this.productService.findAll({
|
||||||
|
limit: 1000,
|
||||||
|
isActive: true,
|
||||||
|
categoryId: dto.categoryId,
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,5 +156,14 @@ export class ProductController {
|
|||||||
return this.productService.remove(id);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import { RequiredEntityData } from '@mikro-orm/core';
|
|||||||
import { Product } from '../entities/product.entity';
|
import { Product } from '../entities/product.entity';
|
||||||
import { CategoryMessage, productMessage } from 'src/common/enums/message.enum';
|
import { CategoryMessage, productMessage } from 'src/common/enums/message.enum';
|
||||||
import { CategoryRepository } from '../repositories/category.repository';
|
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()
|
@Injectable()
|
||||||
export class ProductService {
|
export class ProductService {
|
||||||
@@ -14,6 +18,7 @@ export class ProductService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly productRepository: ProductRepository,
|
private readonly productRepository: ProductRepository,
|
||||||
private readonly categoryRepository: CategoryRepository,
|
private readonly categoryRepository: CategoryRepository,
|
||||||
|
private readonly fieldRepository: FieldRepository,
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@@ -103,4 +108,57 @@ export class ProductService {
|
|||||||
if (!product) throw new NotFoundException(productMessage.NOT_FOUND);
|
if (!product) throw new NotFoundException(productMessage.NOT_FOUND);
|
||||||
return product;
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,36 @@ export class CategoryRepository extends EntityRepository<Category> {
|
|||||||
super(em, Category);
|
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<PaginatedResult<Category>> {
|
async findAllPaginated(opts: FindCategoriesOpts = {}): Promise<PaginatedResult<Category>> {
|
||||||
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc', isActive } = opts;
|
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc', isActive } = opts;
|
||||||
|
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
const where: FilterQuery<Category> = { parent: null };
|
const where: FilterQuery<Category> = {};
|
||||||
|
|
||||||
if (typeof isActive === 'boolean') {
|
if (typeof isActive === 'boolean') {
|
||||||
where.isActive = isActive;
|
where.isActive = isActive;
|
||||||
@@ -37,13 +61,14 @@ export class CategoryRepository extends EntityRepository<Category> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const [data, total] = await this.findAndCount(where, {
|
const allCategories = await this.find(where, {
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
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);
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -55,7 +80,5 @@ export class CategoryRepository extends EntityRepository<Category> {
|
|||||||
totalPages,
|
totalPages,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class ProductRepository extends EntityRepository<Product> {
|
|||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||||
populate: [],
|
populate: ['category'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / limit);
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ export const adminsData: AdminData[] = [
|
|||||||
roleName: 'admin',
|
roleName: 'admin',
|
||||||
|
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// phone: '09121724095',
|
phone: '09121724095',
|
||||||
// firstName: 'آقای',
|
firstName: 'آقای',
|
||||||
// lastName: 'نواعتقاد',
|
lastName: 'نواعتقاد',
|
||||||
// roleName: 'admin',
|
roleName: 'admin',
|
||||||
|
|
||||||
// },
|
},
|
||||||
// {
|
// {
|
||||||
// phone: '09185290775',
|
// phone: '09185290775',
|
||||||
// firstName: 'حمید',
|
// firstName: 'حمید',
|
||||||
|
|||||||
Reference in New Issue
Block a user