diff --git a/src/modules/companies/DTO/update-company.dto.ts b/src/modules/companies/DTO/update-company.dto.ts index 3dd7e42..a448223 100644 --- a/src/modules/companies/DTO/update-company.dto.ts +++ b/src/modules/companies/DTO/update-company.dto.ts @@ -1,5 +1,7 @@ import { PartialType } from "@nestjs/swagger"; -import { CreateCompanyDto } from "./create-company.dto"; +import { CreateCompanyDto, CreateCompanyRequestDto } from "./create-company.dto"; export class UpdateCompanyDto extends PartialType(CreateCompanyDto) {} + +export class UpdateUserCompanyDto extends PartialType(CreateCompanyRequestDto) {} diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts index c51c9b1..5df00c6 100644 --- a/src/modules/companies/companies.controller.ts +++ b/src/modules/companies/companies.controller.ts @@ -3,7 +3,7 @@ import { ApiOperation } from "@nestjs/swagger"; import { CompanyListPublicQueryDto, CompanyListQueryDto } from "./DTO/company-list-query.dto"; import { ChangeCompanyRequestStatusDto, CreateCompanyDto, CreateCompanyRequestDto } from "./DTO/create-company.dto"; -import { UpdateCompanyDto } from "./DTO/update-company.dto"; +import { UpdateCompanyDto, UpdateUserCompanyDto } from "./DTO/update-company.dto"; import { CompaniesService } from "./services/companies.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { BusinessDec } from "../../common/decorators/business.decorator"; @@ -105,6 +105,18 @@ export class CompaniesController { return this.companiesService.getCompanyRequestsUser(userId, businessId); } + @Patch(":id/user") + @AuthGuards() + @ApiOperation({ summary: "update company (user)" }) + updateCompanyByUser( + @Param() paramDto: ParamDto, + @Body() updateDto: UpdateUserCompanyDto, + @UserDec("id") userId: string, + @BusinessDec("id") businessId: string, + ) { + return this.companiesService.updateCompanyByUser(paramDto.id, updateDto, userId, businessId); + } + @Get("requests") @AuthGuards() @ApiOperation({ summary: "get all company registration requests (admin)" }) diff --git a/src/modules/companies/entities/company.entity.ts b/src/modules/companies/entities/company.entity.ts index 5cb8be2..acb7546 100644 --- a/src/modules/companies/entities/company.entity.ts +++ b/src/modules/companies/entities/company.entity.ts @@ -59,13 +59,13 @@ export class Company extends BaseEntity { //----------------------------------- - @OneToMany(() => CompanyProduct, (product) => product.company, { cascade: [Cascade.ALL] }) + @OneToMany(() => CompanyProduct, (product) => product.company, { cascade: [Cascade.ALL], orphanRemoval: true }) products = new Collection(this); - @OneToMany(() => CompanyService, (service) => service.company, { cascade: [Cascade.ALL] }) + @OneToMany(() => CompanyService, (service) => service.company, { cascade: [Cascade.ALL], orphanRemoval: true }) services = new Collection(this); - @OneToMany(() => Invoice, (invoice) => invoice.company, { cascade: [Cascade.ALL] }) + @OneToMany(() => Invoice, (invoice) => invoice.company, { cascade: [Cascade.ALL], orphanRemoval: true }) invoices = new Collection(this); //----------------------------------- diff --git a/src/modules/companies/services/companies.service.ts b/src/modules/companies/services/companies.service.ts index 786901a..988eb45 100644 --- a/src/modules/companies/services/companies.service.ts +++ b/src/modules/companies/services/companies.service.ts @@ -11,7 +11,7 @@ import { UsersService } from "../../users/services/users.service"; import { PasswordService } from "../../utils/providers/password.service"; import { CompanyListPublicQueryDto, CompanyListQueryDto } from "../DTO/company-list-query.dto"; import { ChangeCompanyRequestStatusDto, CreateCompanyDto, CreateCompanyRequestDto } from "../DTO/create-company.dto"; -import { UpdateCompanyDto } from "../DTO/update-company.dto"; +import { UpdateCompanyDto, UpdateUserCompanyDto } from "../DTO/update-company.dto"; import { CompanyProduct } from "../entities/company-product.entity"; import { CompanyRequest } from "../entities/company-request.enitiy"; import { CompanyService } from "../entities/company-service.entity"; @@ -110,39 +110,25 @@ export class CompaniesService { if (Object.keys(userUpdateData).length > 0) await this.usersService.updateUser(company.user.id, userUpdateData, em); if (services && services.length > 0) { - // Remove all old services from the database - for (const oldService of company.services.getItems()) { - em.remove(oldService); - } - await em.flush(); // flush removals before adding new - - // Add new services - for (const service of services) { - const serviceEntity = em.create(CompanyService, { - title: service.title, - imageUrl: service.imageUrl, - company: company, - }); - updatedCompany.services.add(serviceEntity); - } + company.services.removeAll(); + const servicesData = services.map((serviceData) => { + const service = new CompanyService(); + service.title = serviceData.title; + service.imageUrl = serviceData.imageUrl; + return service; + }); + company.services.set(servicesData); } if (products && products.length > 0) { - // Remove all old products from the database - for (const oldProduct of company.products.getItems()) { - em.remove(oldProduct); - } - await em.flush(); // flush removals before adding new - - // Add new products - for (const product of products) { - const productEntity = em.create(CompanyProduct, { - title: product.title, - imageUrl: product.imageUrl, - company: company, - }); - updatedCompany.products.add(productEntity); - } + company.products.removeAll(); + const productsData = products.map((productData) => { + const product = new CompanyProduct(); + product.title = productData.title; + product.imageUrl = productData.imageUrl; + return product; + }); + company.products.set(productsData); } await em.flush(); @@ -171,7 +157,7 @@ export class CompaniesService { { id, deletedAt: null, isActive: true, status: CompanyStatus.APPROVED, business: { id: businessId } }, { populate: ["industry", "user", "products", "services"], - fields: ["*", "industry.id", "industry.title", "user.id", "user.firstName", "user.lastName", "user.phone", "user.email"], + fields: ["*", "industry.id", "industry.title"], }, ); if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); @@ -228,7 +214,7 @@ export class CompaniesService { { user: { id: userId }, business: { id: businessId }, deletedAt: null }, { populate: ["industry", "user", "products", "services"], - fields: ["*", "industry.id", "industry.title", "user.id", "user.firstName", "user.lastName", "user.phone", "user.email"], + fields: ["*", "industry.id", "industry.title"], }, ); if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); @@ -297,6 +283,62 @@ export class CompaniesService { } //----------------------------------- + async updateCompanyByUser(id: string, updateDto: UpdateUserCompanyDto, userId: string, businessId: string) { + const em = this.em.fork(); + try { + await em.begin(); + const { services, products, ...rest } = updateDto; + + const company = await em.findOne(Company, { id, business: { id: businessId }, user: { id: userId }, deletedAt: null }); + if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); + + const [existingCompany, existIdentityNumber, existingEmail, existingPhone] = await Promise.all([ + em.findOne(Company, { name: updateDto.name, business: { id: businessId }, id: { $ne: company.id } }), + em.findOne(Company, { identificationNumber: updateDto.identificationNumber, business: { id: businessId }, id: { $ne: company.id } }), + em.findOne(Company, { email: updateDto.email, business: { id: businessId }, id: { $ne: company.id } }), + em.findOne(Company, { phone: updateDto.phone, business: { id: businessId }, id: { $ne: company.id } }), + ]); + + if (existingCompany) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_NAME); + if (existIdentityNumber) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_IDENTITY_NUMBER); + if (existingEmail) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_EMAIL); + if (existingPhone) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_PHONE); + + em.assign(company, rest); + + if (updateDto.services && updateDto.services.length > 0) { + company.services.removeAll(); + const services = updateDto.services.map((serviceData) => { + const service = new CompanyService(); + service.title = serviceData.title; + service.imageUrl = serviceData.imageUrl; + return service; + }); + company.services.set(services); + } + + if (updateDto.products && updateDto.products.length > 0) { + company.products.removeAll(); + const products = updateDto.products.map((productData) => { + const product = new CompanyProduct(); + product.title = productData.title; + product.imageUrl = productData.imageUrl; + return product; + }); + company.products.set(products); + } + + em.persist(company); + await em.flush(); + await em.commit(); + return { message: CompanyMessage.UPDATED_SUCCESSFULLY, companyId: company.id }; + } catch (error) { + await em.rollback(); + throw error; + } + } + //----------------------------------- + async getCompanyRequestById(id: string, role: RoleEnum, userId: string, businessId: string) { let request = null; @@ -358,7 +400,7 @@ export class CompaniesService { request.company.status = CompanyStatus.APPROVED; } - await em.persistAndFlush(request); + em.persist(request); await em.flush(); await em.commit(); return { message: CompanyMessage.UPDATED_SUCCESSFULLY, status: request.requestStatus }; @@ -373,13 +415,17 @@ export class CompaniesService { const industry = await em.findOne(Industry, { id: createDto.industryId, deletedAt: null }); if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); - const [existingCompany, existIdentityNumber] = await Promise.all([ + const [existingCompany, existIdentityNumber, existingEmail, existingPhone] = await Promise.all([ em.findOne(Company, { name: createDto.name, business: { id: business.id } }), em.findOne(Company, { identificationNumber: createDto.identificationNumber, business: { id: business.id } }), + em.findOne(Company, { email: createDto.email, business: { id: business.id } }), + em.findOne(Company, { phone: createDto.phone, business: { id: business.id } }), ]); if (existingCompany) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_NAME); if (existIdentityNumber) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_IDENTITY_NUMBER); + if (existingEmail) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_EMAIL); + if (existingPhone) throw new BadRequestException(CompanyMessage.COMPANY_ALREADY_EXISTS_PHONE); const products = createDto.products.map((productData) => { const product = new CompanyProduct(); diff --git a/src/modules/invoices/entities/invoice.entity.ts b/src/modules/invoices/entities/invoice.entity.ts index ad0f72f..f969893 100755 --- a/src/modules/invoices/entities/invoice.entity.ts +++ b/src/modules/invoices/entities/invoice.entity.ts @@ -58,7 +58,7 @@ export class Invoice extends BaseEntity { //----------------------------------- - @OneToMany(() => InvoiceItem, (item) => item.invoice, { cascade: [Cascade.PERSIST, Cascade.REMOVE] }) + @OneToMany(() => InvoiceItem, (item) => item.invoice, { cascade: [Cascade.PERSIST, Cascade.REMOVE], orphanRemoval: true }) items = new Collection(this); @OneToMany(() => Payment, (payment) => payment.invoice, { cascade: [Cascade.PERSIST, Cascade.REMOVE] }) diff --git a/src/modules/invoices/enums/invoice-recurring-period.enum.ts b/src/modules/invoices/enums/invoice-recurring-period.enum.ts index 40594e0..80680c8 100755 --- a/src/modules/invoices/enums/invoice-recurring-period.enum.ts +++ b/src/modules/invoices/enums/invoice-recurring-period.enum.ts @@ -1,7 +1,7 @@ export enum RecurringPeriodEnum { - WEEKLY = 1, - MONTHLY, - QUARTERLY, - BIANNUALLY, - ANNUALLY, + WEEKLY = 1, // هفتگی + MONTHLY, // ماهانه + QUARTERLY, // سه‌ماهه + SEMIANNUALLY, // شش‌ماهه + ANNUALLY, // سالانه } diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 98e2602..b5f2fe9 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -131,8 +131,9 @@ export class InvoicesService { if (invoice.status !== InvoiceStatus.PENDING) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_UPDATE); if (updateDto.items) { + this.validateInvoiceItems(updateDto.items); // Remove existing items - await em.nativeDelete(InvoiceItem, { invoice: invoice.id }); + invoice.items.removeAll(); // Create new items const invoiceItems = updateDto.items.map((item) => { @@ -161,6 +162,15 @@ export class InvoicesService { invoice.company = company; } + if (updateDto.isRecurring) { + this.validateRecurringInvoice(updateDto); + await this.updateScheduleInvoiceJobs(invoice, updateDto); + + invoice.isRecurring = updateDto.isRecurring; + invoice.recurringPeriod = updateDto.recurringPeriod; + invoice.maxRecurringCycles = updateDto.maxRecurringCycles ? updateDto.maxRecurringCycles : invoice.maxRecurringCycles; + } + // Save the updated invoice await em.persistAndFlush(invoice); @@ -443,7 +453,7 @@ export class InvoicesService { case RecurringPeriodEnum.QUARTERLY: delayMs = dayjs().add(3, "month").subtract(7, "day").diff(dayjs()); break; - case RecurringPeriodEnum.BIANNUALLY: + case RecurringPeriodEnum.SEMIANNUALLY: delayMs = dayjs().add(6, "month").subtract(7, "day").diff(dayjs()); break; case RecurringPeriodEnum.ANNUALLY: @@ -458,7 +468,7 @@ export class InvoicesService { } //*********************************** */ - private async scheduleInvoiceJobs(invoice: Invoice, createDto?: CreateInvoiceDto) { + private async scheduleInvoiceJobs(invoice: Invoice, createDto?: CreateInvoiceDto | UpdateInvoiceDto) { // Add to queue for billing reminder await this.invoiceQueue.add( INVOICE.REMINDER_JOB_NAME, @@ -489,7 +499,7 @@ export class InvoicesService { }, ); - this.logger.log(`Scheduled recurring invoice for user ${createDto.companyId} with interval ${createDto.recurringPeriod}`); + this.logger.log(`Scheduled recurring invoice for company ${invoice.company.id} with interval ${createDto.recurringPeriod}`); } } //*********************************** */ @@ -507,7 +517,7 @@ export class InvoicesService { } ///********************************** */ - private validateRecurringInvoice(createDto: CreateInvoiceDto): void { + private validateRecurringInvoice(createDto: CreateInvoiceDto | UpdateInvoiceDto): void { if (createDto.isRecurring) { if (!createDto.recurringPeriod) throw new BadRequestException(InvoiceMessage.RECURRING_PERIOD_REQUIRED); @@ -516,4 +526,44 @@ export class InvoicesService { } } } + + //*********************************** */ + private async updateScheduleInvoiceJobs(invoice: Invoice, updateDto: UpdateInvoiceDto) { + // + await this.removeRecurringInvoiceJobs(invoice.id); + await this.removeReminderInvoiceJobs(invoice.id); + + if (updateDto.isRecurring) { + if (!updateDto.recurringPeriod) throw new BadRequestException(InvoiceMessage.RECURRING_PERIOD_REQUIRED); + + if (updateDto.maxRecurringCycles && updateDto.maxRecurringCycles < 1) { + throw new BadRequestException(InvoiceMessage.MAX_RECURRING_CYCLES_MUST_BE_POSITIVE); + } + + await this.scheduleInvoiceJobs(invoice, updateDto); + + this.logger.log(`Updated recurring invoice for company ${updateDto.companyId} with interval ${updateDto.recurringPeriod}`); + } + } + + //*********************************** */ + + private async removeRecurringInvoiceJobs(invoiceId: string) { + const existingJobs = await this.invoiceQueue.getJobs(["delayed", "waiting", "active"]); + for (const job of existingJobs) { + if (job.name === INVOICE.RECURRING_JOB_NAME && job.data.invoiceId === invoiceId) { + await job.remove(); + } + } + } + //*********************************** */ + + private async removeReminderInvoiceJobs(invoiceId: string) { + const existingJobs = await this.invoiceQueue.getJobs(["delayed", "waiting", "active"]); + for (const job of existingJobs) { + if (job.name === INVOICE.REMINDER_JOB_NAME && job.data.invoiceId === invoiceId) { + await job.remove(); + } + } + } }