chore: add announcement logic
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm/oCsqFA8hF9TMv4UX5E
|
||||
IeDv1KXzI+KFIafZ+gU1IWgEmWP16ee8Vw1+j7Ixl9Dd7Uxc9DhcWRB6ZDwqCYb3
|
||||
S3z5zabOIka7wNNBpTEmRKrmV/EzihAw+E96Ksct0PHeN2YFydPimMndBD6VoRWi
|
||||
WShQD4FklIQKqBYrua0NkuIkFshYLzC3rAbcT8GkLkcbHKrKIlyeucCLwym2nd5U
|
||||
coI+fKqDB1/3JYEKJRaZgFqthokeFBN4SV9tMaLzeJDk3bNoTAxRtPbtuJ8mVTL9
|
||||
P0phzjorGxTxV7SbANtmB3Trm6oIXeCqY40Auy+ZTUEVr+hD4Xxs8TMvjuT8vrMe
|
||||
VQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -14,6 +14,7 @@ import { mailerConfig } from "./configs/mailer.config";
|
||||
import { databaseConfig } from "./configs/mikro-orm.config";
|
||||
import { rateLimitConfig } from "./configs/rateLimit.config";
|
||||
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
||||
import { AnnouncementsModule } from "./modules/announcements/announcement.module";
|
||||
import { AuthModule } from "./modules/auth/auth.module";
|
||||
import { BusinessesModule } from "./modules/businesses/businesses.module";
|
||||
import { CompaniesModule } from "./modules/companies/companies.module";
|
||||
@@ -53,6 +54,7 @@ import { UtilsModule } from "./modules/utils/utils.module";
|
||||
TicketsModule,
|
||||
InvoicesModule,
|
||||
BusinessesModule,
|
||||
AnnouncementsModule,
|
||||
],
|
||||
// providers: [
|
||||
// {
|
||||
|
||||
@@ -3,4 +3,6 @@ export const AUTH_THROTTLE_TTL = 5 * 60 * 1000;
|
||||
export const AUTH_THROTTLE_LIMIT = 5;
|
||||
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
||||
export const AUTH__REFRESH_THROTTLE_LIMIT = 10;
|
||||
export const JWT_STRATEGY_NAME = "jwt_Strategy";
|
||||
//
|
||||
export const CONSOLE_JWT_STRATEGY_NAME = "console_jwt_strategy";
|
||||
export const LOCAL_JWT_STRATEGY_NAME = "local_jwt_strategy";
|
||||
|
||||
@@ -236,6 +236,8 @@ export const enum AnnouncementMessage {
|
||||
PUBLISH_AT_CANNOT_BE_IN_PAST = "تاریخ انتشار نمیتواند در گذشته باشد",
|
||||
USER_ANNOUNCEMENTS_DELETED = "اطلاعیههای کاربر با موفقیت حذف شدند",
|
||||
QUEUE_JOBS_DELETED = "وظایف صف با موفقیت حذف شدند",
|
||||
COMPANY_MUST_BE_ID = "شناسه شرکت اجباری است",
|
||||
COMPANY_MUST_BE_UUID = "شناسه شرکت باید یک UUID معتبر باشد",
|
||||
}
|
||||
|
||||
export const enum CriticismMessage {
|
||||
@@ -293,6 +295,8 @@ export const enum TicketMessageEnum {
|
||||
INVALID_TICKET_LIMIT = "محدودیت تعداد تیکت در پلن پشتیبانی شما نامعتبر است",
|
||||
TICKET_LIMIT_EXCEEDED = "شما به محدودیت تعداد تیکت در پلن پشتیبانی خود رسیدهاید",
|
||||
OPENED = "تیکت با موفقیت باز شد",
|
||||
CATEGORY_HAS_TICKETS = "دسته بندی دارای تیکت می باشد",
|
||||
CATEGORY_DELETED = "دسته بندی با موفقیت حذف شد",
|
||||
}
|
||||
|
||||
export const enum WalletMessage {
|
||||
|
||||
@@ -20,7 +20,7 @@ export function mailerConfig(): MailerAsyncOptions {
|
||||
},
|
||||
|
||||
template: {
|
||||
dir: process.cwd() + "/src/templates/email",
|
||||
dir: process.cwd() + "/src/modules/templates/email",
|
||||
adapter: new HandlebarsAdapter(),
|
||||
options: {
|
||||
strict: true,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsDateString, IsNotEmpty, IsOptional, IsString, Length, MinLength } from "class-validator";
|
||||
|
||||
import { AnnouncementMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class CreateAnnouncementDto {
|
||||
@IsNotEmpty({ message: AnnouncementMessage.TITLE_IS_REQUIRED })
|
||||
@IsString({ message: AnnouncementMessage.TITLE_STRING })
|
||||
@Length(5, 100, { message: AnnouncementMessage.TITLE_LENGTH })
|
||||
@ApiProperty({ description: "Title of the announcement", example: "اطلاعیه جدید" })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ message: AnnouncementMessage.CONTENT_IS_REQUIRED })
|
||||
@IsString({ message: AnnouncementMessage.CONTENT_IS_STRING })
|
||||
@MinLength(10, { message: AnnouncementMessage.CONTENT_MUST_BE_LONGER })
|
||||
@ApiProperty({ description: "Content of the announcement", example: "اطلاعیه جدید برای کاربران" })
|
||||
content: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: AnnouncementMessage.PUBLISH_AT_IS_REQUIRED })
|
||||
@IsDateString({}, { message: AnnouncementMessage.PUBLISH_AT_MUST_BE_DATE })
|
||||
@ApiProperty({ description: "Publish date of the announcement", example: "2023-10-01T10:00:00Z" })
|
||||
publishAt: string;
|
||||
|
||||
@IsNotEmpty({ message: AnnouncementMessage.IMPORTANT_IS_REQUIRED })
|
||||
@IsBoolean({ message: AnnouncementMessage.IMPORTANT_MUST_BE_BOOLEAN })
|
||||
@ApiProperty({ description: "Is this announcement important?", example: false })
|
||||
isImportant: boolean;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsDateString, IsOptional, IsString } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
import { CriticismMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class SearchAnnouncementQueryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
@ApiPropertyOptional({ description: "since query", example: "2025-01-27" })
|
||||
since?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
@ApiPropertyOptional({ description: "publishAt query", example: "2025-01-27" })
|
||||
publishAt?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: CriticismMessage.SEARCH_QUERY_STRING })
|
||||
@ApiPropertyOptional({ description: "Search query", example: "search query" })
|
||||
q?: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateAnnouncementDto } from "./create-announcement.dto";
|
||||
|
||||
export class UpdateAnnouncementDto extends PartialType(CreateAnnouncementDto) {}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
import { CreateAnnouncementDto } from "./DTO/create-announcement.dto";
|
||||
import { SearchAnnouncementQueryDto } from "./DTO/search-announcement-query.dto";
|
||||
import { UpdateAnnouncementDto } from "./DTO/update-announcement.dto";
|
||||
import { AnnouncementService } from "./providers/announcement.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
||||
import { Business } from "../businesses/entities/business.entity";
|
||||
|
||||
@Controller("announcements")
|
||||
@UseInterceptors(BusinessInterceptor)
|
||||
@AuthGuards()
|
||||
export class AnnouncementController {
|
||||
constructor(private readonly announcementService: AnnouncementService) {}
|
||||
|
||||
@ApiProperty({ description: "Create a new announcement (admin route)" })
|
||||
@Post()
|
||||
create(@Body() createAnnouncementDto: CreateAnnouncementDto, @BusinessDec() business: Business) {
|
||||
return this.announcementService.createAnnouncement(createAnnouncementDto, business);
|
||||
}
|
||||
|
||||
@ApiProperty({ description: "Update an announcement (admin route)" })
|
||||
@Patch(":id")
|
||||
update(@Param() paramDto: ParamDto, @Body() updateAnnouncementDto: UpdateAnnouncementDto, @BusinessDec("id") businessId: string) {
|
||||
return this.announcementService.updateAnnouncement(paramDto.id, updateAnnouncementDto, businessId);
|
||||
}
|
||||
|
||||
@ApiProperty({ description: "Delete an announcement (admin route)" })
|
||||
@Delete(":id")
|
||||
delete(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||
return this.announcementService.deleteAnnouncement(paramDto.id, businessId);
|
||||
}
|
||||
|
||||
@ApiProperty({ description: "Get all announcements (admin route)" })
|
||||
@Get()
|
||||
getAnnouncements(@Query() queryDto: SearchAnnouncementQueryDto, @BusinessDec("id") businessId: string) {
|
||||
return this.announcementService.getAnnouncements(queryDto, businessId);
|
||||
}
|
||||
|
||||
@ApiProperty({ description: "Get all announcements for user " })
|
||||
@Get("user")
|
||||
getAnnouncementsByUser(
|
||||
@Query() queryDto: SearchAnnouncementQueryDto,
|
||||
@UserDec("id") userId: string,
|
||||
@BusinessDec("id") businessId: string,
|
||||
) {
|
||||
return this.announcementService.getAnnouncementsByUser(queryDto, userId, businessId);
|
||||
}
|
||||
|
||||
@ApiProperty({ description: "Get one announcements with id " })
|
||||
@Get(":id")
|
||||
getAnnouncementById(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @BusinessDec("id") businessId: string) {
|
||||
return this.announcementService.getAnnouncementById(paramDto.id, userId, businessId);
|
||||
}
|
||||
|
||||
@ApiProperty({ description: "Get one announcements with id (admin route)" })
|
||||
@Get(":id/admin")
|
||||
getAnnouncementByIdAdmin(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||
return this.announcementService.getAnnouncementByIdAdmin(paramDto.id, businessId);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { BullModule } from "@nestjs/bullmq";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { AnnouncementController } from "./announcement.controller";
|
||||
import { ANNOUNCEMENT } from "./constants";
|
||||
import { Announcement } from "./entities/announcement.entity";
|
||||
import { AnnouncementService } from "./providers/announcement.service";
|
||||
import { AnnouncementProcessor } from "./queue/announcment.processor";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { UserAnnouncement } from "./entities/user-announcement.entity";
|
||||
import { BusinessesModule } from "../businesses/businesses.module";
|
||||
import { CompaniesModule } from "../companies/companies.module";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Announcement, UserAnnouncement]),
|
||||
BullModule.registerQueue({ name: ANNOUNCEMENT.ANNOUNCEMENT_QUEUE_NAME }),
|
||||
UsersModule,
|
||||
NotificationModule,
|
||||
CompaniesModule,
|
||||
BusinessesModule,
|
||||
],
|
||||
providers: [AnnouncementService, AnnouncementProcessor],
|
||||
controllers: [AnnouncementController],
|
||||
exports: [AnnouncementService],
|
||||
})
|
||||
export class AnnouncementsModule {}
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
export const ANNOUNCEMENT = Object.freeze({
|
||||
ANNOUNCEMENT_QUEUE_NAME: "announcement",
|
||||
ANNOUNCEMENT_SEND_JOB_NAME: "sendAnnouncement",
|
||||
ANNOUNCEMENT_PUBLISH_JOB_NAME: "publishAnnouncement",
|
||||
//
|
||||
ANNOUNCEMENT_SEND_JOB_PRIORITY: 1,
|
||||
ANNOUNCEMENT_SEND_JOB_ATTEMPTS: 3,
|
||||
ANNOUNCEMENT_SEND_JOB_BACKOFF: 5 * 1000,
|
||||
ANNOUNCEMENT_SEND_JOB_TIMEOUT: 10000,
|
||||
//
|
||||
ANNOUNCEMENT_PUBLISH_JOB_PRIORITY: 1,
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Cascade, Collection, Entity, EntityRepositoryType, Index, ManyToOne, OneToMany, Property } from "@mikro-orm/core";
|
||||
|
||||
import { UserAnnouncement } from "./user-announcement.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { Business } from "../../businesses/entities/business.entity";
|
||||
import { AnnouncementRepository } from "../repositories/announcement.repository";
|
||||
|
||||
@Entity({ repository: () => AnnouncementRepository })
|
||||
@Index({ properties: ["isPublic", "createdAt"] })
|
||||
export class Announcement extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 100 })
|
||||
title!: string;
|
||||
|
||||
@Property({ type: "text" })
|
||||
content!: string;
|
||||
|
||||
@Property({ type: "timestamptz", nullable: true, default: null })
|
||||
publishAt?: Date;
|
||||
|
||||
@Property({ type: "boolean", default: false })
|
||||
isImportant!: boolean;
|
||||
|
||||
@ManyToOne(() => Business, { deleteRule: "cascade", nullable: false })
|
||||
business!: Business;
|
||||
|
||||
@OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.announcement, { cascade: [Cascade.ALL] })
|
||||
userAnnouncements = new Collection<UserAnnouncement>(this);
|
||||
|
||||
@Property({ type: "boolean", default: false })
|
||||
isPublic!: boolean;
|
||||
|
||||
[EntityRepositoryType]?: AnnouncementRepository;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Entity, EntityRepositoryType, ManyToOne, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { Announcement } from "./announcement.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { UserAnnouncementRepository } from "../repositories/user-announcement.repository";
|
||||
|
||||
@Entity({ repository: () => UserAnnouncementRepository })
|
||||
export class UserAnnouncement extends BaseEntity {
|
||||
@ManyToOne(() => User, { deleteRule: "cascade", nullable: false })
|
||||
user!: User;
|
||||
|
||||
@ManyToOne(() => Announcement, { deleteRule: "cascade", nullable: false })
|
||||
announcement!: Announcement;
|
||||
|
||||
@Property({ type: "boolean", default: false })
|
||||
isRead!: boolean & Opt;
|
||||
|
||||
[EntityRepositoryType]?: UserAnnouncementRepository;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ISendAnnouncement {
|
||||
announcementId: string;
|
||||
isPublic: boolean;
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
import { EntityManager, FilterQuery } from "@mikro-orm/postgresql";
|
||||
import { InjectQueue } from "@nestjs/bullmq";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { Queue } from "bullmq";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { AnnouncementMessage } from "../../../common/enums/message.enum";
|
||||
import { Business } from "../../businesses/entities/business.entity";
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { ANNOUNCEMENT } from "../constants";
|
||||
import { CreateAnnouncementDto } from "../DTO/create-announcement.dto";
|
||||
import { SearchAnnouncementQueryDto } from "../DTO/search-announcement-query.dto";
|
||||
import { UpdateAnnouncementDto } from "../DTO/update-announcement.dto";
|
||||
import { Announcement } from "../entities/announcement.entity";
|
||||
import { UserAnnouncement } from "../entities/user-announcement.entity";
|
||||
import { AnnouncementRepository } from "../repositories/announcement.repository";
|
||||
import { UserAnnouncementRepository } from "../repositories/user-announcement.repository";
|
||||
|
||||
@Injectable()
|
||||
export class AnnouncementService {
|
||||
constructor(
|
||||
@InjectQueue(ANNOUNCEMENT.ANNOUNCEMENT_QUEUE_NAME) private readonly announcementQueue: Queue,
|
||||
private readonly announcementRepository: AnnouncementRepository,
|
||||
private readonly userAnnouncementRepository: UserAnnouncementRepository,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
/***************************************** */
|
||||
|
||||
async createAnnouncement(createAnnouncementDto: CreateAnnouncementDto, business: Business) {
|
||||
const isPublic = true;
|
||||
|
||||
const announcement = this.announcementRepository.create({
|
||||
...createAnnouncementDto,
|
||||
business,
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
if (!announcement.publishAt) {
|
||||
announcement.publishAt = dayjs().toDate();
|
||||
}
|
||||
|
||||
if (dayjs(announcement.publishAt).isBefore(dayjs().subtract(1, "day")))
|
||||
throw new BadRequestException(AnnouncementMessage.PUBLISH_AT_MUST_BE_FUTURE_DATE);
|
||||
|
||||
await this.em.persistAndFlush(announcement);
|
||||
|
||||
await this.announcementQueue.add(
|
||||
ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_NAME,
|
||||
{
|
||||
announcementId: announcement.id,
|
||||
isPublic,
|
||||
},
|
||||
{
|
||||
delay: dayjs(announcement.publishAt).diff(dayjs(), "millisecond"),
|
||||
attempts: ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_ATTEMPTS,
|
||||
backoff: { type: "exponential", delay: ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_BACKOFF },
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
message: AnnouncementMessage.CREATED_AND_SENT_TO_USERS_AFTER_PUBLISH,
|
||||
announcement,
|
||||
};
|
||||
}
|
||||
/***************************************** */
|
||||
|
||||
async getAnnouncements(queryDto: SearchAnnouncementQueryDto, businessId: string) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.announcementRepository.createQueryBuilder("announcement");
|
||||
|
||||
queryBuilder.andWhere({ business: { id: businessId } });
|
||||
|
||||
if (queryDto.since) {
|
||||
queryBuilder.andWhere({ createdAt: { $gte: dayjs(queryDto.since).toDate() } });
|
||||
}
|
||||
|
||||
if (queryDto.publishAt) {
|
||||
queryBuilder.andWhere({ publishAt: { $gte: dayjs(queryDto.publishAt).toDate() } });
|
||||
}
|
||||
|
||||
if (queryDto.q) {
|
||||
queryBuilder.andWhere({ title: { $ilike: `%${queryDto.q}%` } });
|
||||
}
|
||||
|
||||
queryBuilder.orderBy({ createdAt: "DESC" });
|
||||
|
||||
const [announcements, count] = await queryBuilder.offset(skip).limit(limit).getResultAndCount();
|
||||
|
||||
return { announcements, count, paginate: true };
|
||||
}
|
||||
|
||||
/***************************************** */
|
||||
|
||||
async getAnnouncementsByUser(queryDto: SearchAnnouncementQueryDto, userId: string, businessId: string) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const where: FilterQuery<UserAnnouncement> = {
|
||||
user: { id: userId },
|
||||
};
|
||||
|
||||
const announcementConditions: FilterQuery<Announcement> = {
|
||||
business: { id: businessId },
|
||||
};
|
||||
|
||||
if (queryDto.since) {
|
||||
announcementConditions.createdAt = { $gte: dayjs(queryDto.since).toDate() };
|
||||
}
|
||||
|
||||
if (queryDto.publishAt) {
|
||||
announcementConditions.publishAt = { $gte: dayjs(queryDto.publishAt).toDate() };
|
||||
}
|
||||
|
||||
if (queryDto.q) {
|
||||
announcementConditions.title = { $ilike: `%${queryDto.q}%` };
|
||||
}
|
||||
|
||||
where.announcement = announcementConditions;
|
||||
|
||||
const [announcements, count] = await this.userAnnouncementRepository.findAndCount(where, {
|
||||
populate: ["announcement", "user"],
|
||||
limit,
|
||||
offset: skip,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
fields: [
|
||||
"id",
|
||||
"isRead",
|
||||
"createdAt",
|
||||
"user.id",
|
||||
"user.firstName",
|
||||
"user.lastName",
|
||||
"announcement.id",
|
||||
"announcement.title",
|
||||
"announcement.isImportant",
|
||||
"announcement.createdAt",
|
||||
"announcement.publishAt",
|
||||
"announcement.content",
|
||||
],
|
||||
});
|
||||
|
||||
return { announcements, count, paginate: true };
|
||||
}
|
||||
|
||||
/***************************************** */
|
||||
|
||||
async getAnnouncementById(id: string, userId: string, businessId: string) {
|
||||
const announcement = await this.announcementRepository.findOne({ id, business: { id: businessId } });
|
||||
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||
|
||||
const userAnnouncement = await this.userAnnouncementRepository.findOne({ announcement: { id }, user: { id: userId } });
|
||||
if (!userAnnouncement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||
|
||||
userAnnouncement.isRead = true;
|
||||
await this.em.flush();
|
||||
return { announcement };
|
||||
}
|
||||
|
||||
/***************************************** */
|
||||
async getAnnouncementByIdAdmin(id: string, businessId: string) {
|
||||
const announcement = await this.announcementRepository.findOne({ id, business: { id: businessId } });
|
||||
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||
//
|
||||
return {
|
||||
announcement,
|
||||
};
|
||||
}
|
||||
|
||||
//************************************ */
|
||||
async countUserAnnouncements(userId: string, businessId: string) {
|
||||
return this.userAnnouncementRepository.count({ user: { id: userId }, announcement: { business: { id: businessId } }, isRead: false });
|
||||
}
|
||||
//************************************ */
|
||||
|
||||
async updateAnnouncement(id: string, updateAnnouncementDto: UpdateAnnouncementDto, businessId: string) {
|
||||
const announcement = await this.announcementRepository.findOne({ id, business: { id: businessId } });
|
||||
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||
|
||||
if (updateAnnouncementDto.publishAt) {
|
||||
if (dayjs(updateAnnouncementDto.publishAt).isBefore(dayjs().subtract(1, "day"))) {
|
||||
throw new BadRequestException(AnnouncementMessage.PUBLISH_AT_MUST_BE_FUTURE_DATE);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateAnnouncementDto.publishAt) {
|
||||
const jobs = await this.announcementQueue.getJobs();
|
||||
const existingJob = jobs.find((job) => job.data.announcementId === id);
|
||||
|
||||
//
|
||||
if (existingJob) await existingJob.remove();
|
||||
|
||||
await this.announcementQueue.add(
|
||||
ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_NAME,
|
||||
{
|
||||
announcementId: announcement.id,
|
||||
isPublic: announcement.isPublic,
|
||||
},
|
||||
{
|
||||
delay: dayjs(announcement.publishAt).diff(dayjs(), "millisecond"),
|
||||
attempts: ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_ATTEMPTS,
|
||||
backoff: { type: "exponential", delay: ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_BACKOFF },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
this.announcementRepository.assign(announcement, updateAnnouncementDto);
|
||||
|
||||
await this.em.flush();
|
||||
|
||||
return {
|
||||
message: AnnouncementMessage.UPDATED_SUCCESSFULLY,
|
||||
announcement,
|
||||
};
|
||||
}
|
||||
|
||||
//************************************ */
|
||||
|
||||
async deleteAnnouncement(id: string, businessId: string) {
|
||||
const announcement = await this.announcementRepository.findOne({ id, business: { id: businessId } });
|
||||
if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND);
|
||||
|
||||
const jobs = await this.announcementQueue.getJobs();
|
||||
const existingJob = jobs.find((job) => job.data.announcementId === id);
|
||||
if (existingJob) await existingJob.remove();
|
||||
|
||||
await this.userAnnouncementRepository.nativeDelete({ announcement: { id } });
|
||||
|
||||
await this.announcementRepository.nativeDelete({ id, business: { id: businessId } });
|
||||
await this.em.flush();
|
||||
|
||||
return {
|
||||
message: AnnouncementMessage.DELETED_SUCCESSFULLY,
|
||||
};
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Processor } from "@nestjs/bullmq";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Job } from "bullmq";
|
||||
|
||||
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
||||
import { IAnnouncementNotificationData } from "../../notifications/interfaces/ISendNotificationData";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { UsersService } from "../../users/services/users.service";
|
||||
import { ANNOUNCEMENT } from "../constants";
|
||||
import { Announcement } from "../entities/announcement.entity";
|
||||
import { UserAnnouncement } from "../entities/user-announcement.entity";
|
||||
import { ISendAnnouncement } from "../interfaces/ISendAnnouncement";
|
||||
|
||||
@Processor(ANNOUNCEMENT.ANNOUNCEMENT_QUEUE_NAME)
|
||||
export class AnnouncementProcessor extends WorkerProcessor {
|
||||
protected readonly logger = new Logger(AnnouncementProcessor.name);
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly notificationQueue: NotificationQueue,
|
||||
private readonly usersService: UsersService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job, token?: string) {
|
||||
switch (job.name) {
|
||||
case ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_NAME:
|
||||
return await this.sendAnnouncement(job, token);
|
||||
case ANNOUNCEMENT.ANNOUNCEMENT_PUBLISH_JOB_NAME:
|
||||
return this.publishAnnouncement(job, token);
|
||||
default:
|
||||
this.logger.error(`Unknown job name: ${job.name}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
//********************************** */
|
||||
private async sendAnnouncement(job: Job<ISendAnnouncement>, token?: string) {
|
||||
this.logger.log(`Sending announcement: ${job.data.announcementId} to users. ${token}`);
|
||||
|
||||
const em = this.em.fork();
|
||||
|
||||
const data = job.data;
|
||||
|
||||
try {
|
||||
await em.begin();
|
||||
// Create a query builder to fetch unique users
|
||||
const userQueryBuilder = em.createQueryBuilder(User, "u");
|
||||
userQueryBuilder.select("u.id").distinct();
|
||||
|
||||
// Execute the query to get distinct user IDs
|
||||
const userResult = await userQueryBuilder.execute();
|
||||
const userIds = userResult.map((row: { id: string }) => row.id);
|
||||
|
||||
if (userIds.length === 0) {
|
||||
this.logger.error(`No users found`);
|
||||
await em.rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
const announcementId = data.announcementId;
|
||||
this.logger.debug(`Sending announcement to ${userIds.length} users`);
|
||||
|
||||
// Create UserAnnouncement entities
|
||||
const userAnnouncements = userIds.map((userId: string) => {
|
||||
const userAnnouncement = new UserAnnouncement();
|
||||
userAnnouncement.user = em.getReference(User, userId);
|
||||
userAnnouncement.announcement = em.getReference(Announcement, announcementId);
|
||||
userAnnouncement.isRead = false;
|
||||
return userAnnouncement;
|
||||
});
|
||||
|
||||
await em.persistAndFlush(userAnnouncements);
|
||||
|
||||
// Fetch announcement details for notification
|
||||
const announcement = await em.findOne(Announcement, { id: job.data.announcementId });
|
||||
if (!announcement) throw new Error("Announcement not found");
|
||||
|
||||
// Send notification to each user
|
||||
for (const userId of userIds) {
|
||||
const user = await this.usersService.findOneByIdWithEntityManager(userId, em);
|
||||
const notifData: IAnnouncementNotificationData = {
|
||||
userPhone: user.phone,
|
||||
userEmail: user.email,
|
||||
title: announcement.title,
|
||||
description: announcement.content.replace(/<[^>]*>/g, ""),
|
||||
date: announcement.publishAt ?? new Date(),
|
||||
};
|
||||
await this.notificationQueue.addAnnouncementNotification(userId, notifData);
|
||||
}
|
||||
|
||||
this.logger.log(`Successfully sent announcement ${announcementId} to ${userIds.length} users`);
|
||||
await em.commit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send announcement:`, error instanceof Error ? error.message : "Unknown error");
|
||||
await em.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
//********************************** */
|
||||
|
||||
private async publishAnnouncement(job: Job, token?: string) {
|
||||
this.logger.log(`Publishing announcement: ${job.data.announcementId} ${token}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
|
||||
import { Announcement } from "../entities/announcement.entity";
|
||||
|
||||
export class AnnouncementRepository extends EntityRepository<Announcement> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
|
||||
import { UserAnnouncement } from "../entities/user-announcement.entity";
|
||||
|
||||
export class UserAnnouncementRepository extends EntityRepository<UserAnnouncement> {}
|
||||
@@ -7,14 +7,15 @@ import { jwtConfig } from "../../configs/jwt.config";
|
||||
import { UtilsModule } from "../utils/utils.module";
|
||||
import { AuthService } from "./providers/auth.service";
|
||||
import { TokensService } from "./providers/tokens.service";
|
||||
import { JwtStrategy } from "./strategies/jwt.strategy";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { ConsoleJwtStrategy } from "./strategies/console-jwt.strategy";
|
||||
import { LocalJwtStrategy } from "./strategies/local-jwt.strategy";
|
||||
|
||||
@Module({
|
||||
imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig()), NotificationModule],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, TokensService, JwtStrategy],
|
||||
providers: [AuthService, TokensService, LocalJwtStrategy, ConsoleJwtStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Reflector } from "@nestjs/core";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
import { CONSOLE_JWT_STRATEGY_NAME, LOCAL_JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
import { SKIP_AUTH_KEY } from "../../../common/decorators/skip-auth.decorator";
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) {
|
||||
export class JwtAuthGuard extends AuthGuard([CONSOLE_JWT_STRATEGY_NAME, LOCAL_JWT_STRATEGY_NAME]) {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
|
||||
// import { Reflector } from "@nestjs/core";
|
||||
// import { FastifyRequest } from "fastify";
|
||||
|
||||
// import { PERMISSION_KEY } from "../../../common/decorators/permission.decorator";
|
||||
// import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
// import { PermissionEnum } from "../../users/enums/permission.enum";
|
||||
|
||||
// @Injectable()
|
||||
// export class PermissionsGuard implements CanActivate {
|
||||
// constructor(private reflector: Reflector) {}
|
||||
|
||||
// canActivate(context: ExecutionContext) {
|
||||
// const requiredPermissions = this.reflector.getAllAndOverride<PermissionEnum[]>(PERMISSION_KEY, [
|
||||
// context.getHandler(),
|
||||
// context.getClass(),
|
||||
// ]);
|
||||
|
||||
// if (!requiredPermissions) return true;
|
||||
|
||||
// const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
// const user = request.user;
|
||||
|
||||
// if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
// const hasPermission = requiredPermissions.every((perm) => user.permissions.includes(perm));
|
||||
|
||||
// if (!hasPermission) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
@@ -1,6 +1,14 @@
|
||||
import { RoleEnum } from "../../users/enums/role.enum";
|
||||
|
||||
export interface ITokenPayload {
|
||||
export interface ILocalTokenPayload {
|
||||
id: string;
|
||||
role: RoleEnum;
|
||||
}
|
||||
|
||||
export interface IConsoleTokenPayload {
|
||||
id: string;
|
||||
permissions?: string[];
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
export interface ITokenPayload extends ILocalTokenPayload, IConsoleTokenPayload {}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
|
||||
import { CONSOLE_JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
import { IConsoleTokenPayload } from "../interfaces/IToken-payload";
|
||||
|
||||
@Injectable()
|
||||
export class ConsoleJwtStrategy extends PassportStrategy(Strategy, CONSOLE_JWT_STRATEGY_NAME) {
|
||||
constructor() {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: readFileSync(`${process.cwd()}/keys/public.pem`, "utf8"),
|
||||
algorithms: ["RS256"],
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: IConsoleTokenPayload) {
|
||||
return { id: payload.id, permissions: payload.permissions, isAdmin: payload.isAdmin };
|
||||
}
|
||||
}
|
||||
Executable → Regular
+2
-2
@@ -3,11 +3,11 @@ import { ConfigService } from "@nestjs/config";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
|
||||
import { JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
import { LOCAL_JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
import { ITokenPayload } from "../interfaces/IToken-payload";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) {
|
||||
export class LocalJwtStrategy extends PassportStrategy(Strategy, LOCAL_JWT_STRATEGY_NAME) {
|
||||
constructor(configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Cascade, Collection, Entity, EntityRepositoryType, OneToMany, Property, Unique } from "@mikro-orm/core";
|
||||
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { Announcement } from "../../announcements/entities/announcement.entity";
|
||||
import { Company } from "../../companies/entities/company.entity";
|
||||
import { Industry } from "../../industries/entities/industry.entity";
|
||||
import { Invoice } from "../../invoices/entities/invoice.entity";
|
||||
import { TicketCategory } from "../../tickets/entities/ticket-category.entity";
|
||||
import { Ticket } from "../../tickets/entities/ticket.entity";
|
||||
import { BusinessRepository } from "../repositories/business.repository";
|
||||
|
||||
@Entity({ repository: () => BusinessRepository })
|
||||
@@ -23,5 +27,17 @@ export class Business extends BaseEntity {
|
||||
@OneToMany(() => Industry, (industry) => industry.business)
|
||||
industries = new Collection<Industry>(this);
|
||||
|
||||
@OneToMany(() => Invoice, (invoice) => invoice.business)
|
||||
invoices = new Collection<Invoice>(this);
|
||||
|
||||
@OneToMany(() => TicketCategory, (ticketCategory) => ticketCategory.business)
|
||||
ticketCategories = new Collection<TicketCategory>(this);
|
||||
|
||||
@OneToMany(() => Ticket, (ticket) => ticket.business)
|
||||
tickets = new Collection<Ticket>(this);
|
||||
|
||||
@OneToMany(() => Announcement, (announcement) => announcement.business)
|
||||
announcements = new Collection<Announcement>(this);
|
||||
|
||||
[EntityRepositoryType]?: BusinessRepository;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CompanyListQueryDto } from "./DTO/company-list-query.dto";
|
||||
import { CreateCompanyDto } from "./DTO/create-company.dto";
|
||||
import { UpdateCompanyDto } 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";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
||||
@@ -15,36 +16,42 @@ export class CompaniesController {
|
||||
constructor(private readonly companiesService: CompaniesService) {}
|
||||
|
||||
@Post()
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "create company (admin)" })
|
||||
create(@Body() createCompanyDto: CreateCompanyDto, @BusinessDec() business: Business) {
|
||||
return this.companiesService.create(createCompanyDto, business);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "update company (admin)" })
|
||||
update(@Param() paramDto: ParamDto, @Body() updateCompanyDto: UpdateCompanyDto, @BusinessDec("id") businessId: string) {
|
||||
return this.companiesService.updateCompanyById(paramDto.id, updateCompanyDto, businessId);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "delete company (admin)" })
|
||||
delete(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||
return this.companiesService.deleteCompanyById(paramDto.id, businessId);
|
||||
}
|
||||
|
||||
@Get("list")
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "get companies list (admin)" })
|
||||
getCompaniesList(@Query() query: CompanyListQueryDto, @BusinessDec("id") businessId: string) {
|
||||
return this.companiesService.getCompaniesList(query, businessId);
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "get company by id" })
|
||||
get(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||
return this.companiesService.getCompanyById(paramDto.id, businessId);
|
||||
}
|
||||
|
||||
@Patch(":id/toggle-status")
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "toggle company status (admin)" })
|
||||
toggleStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||
return this.companiesService.toggleStatus(paramDto.id, businessId);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CreateIndustryDto } from "./DTO/create-industry.dto";
|
||||
import { IndustryListQueryDto } from "./DTO/industry-list-query.dto";
|
||||
import { UpdateIndustryDto } from "./DTO/update-industry.dto";
|
||||
import { IndustriesService } from "./services/industries.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
||||
@@ -16,6 +17,7 @@ export class IndustriesController {
|
||||
constructor(private readonly industriesService: IndustriesService) {}
|
||||
|
||||
@Post()
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "create industry" })
|
||||
create(@Body() createIndustryDto: CreateIndustryDto, @BusinessDec() business: Business) {
|
||||
return this.industriesService.create(createIndustryDto, business);
|
||||
@@ -28,24 +30,28 @@ export class IndustriesController {
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "update industry" })
|
||||
updateIndustry(@Param() paramDto: ParamDto, @Body() updateIndustryDto: UpdateIndustryDto, @BusinessDec("id") businessId: string) {
|
||||
return this.industriesService.updateIndustry(paramDto.id, updateIndustryDto, businessId);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "delete industry" })
|
||||
deleteIndustry(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||
return this.industriesService.deleteIndustry(paramDto.id, businessId);
|
||||
}
|
||||
|
||||
@Get("list")
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "get industries list for admin" })
|
||||
getIndustriesForAdmin(@Query() query: IndustryListQueryDto, @BusinessDec("id") businessId: string) {
|
||||
return this.industriesService.getIndustriesListForAdmin(query, businessId);
|
||||
}
|
||||
|
||||
@Patch(":id/toggle-status")
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "toggle industry status" })
|
||||
toggleStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||
return this.industriesService.toggleStatus(paramDto.id, businessId);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Decimal } from "decimal.js";
|
||||
|
||||
import { InvoiceItem } from "./invoice-item.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { Business } from "../../businesses/entities/business.entity";
|
||||
import { Company } from "../../companies/entities/company.entity";
|
||||
import { RecurringPeriodEnum } from "../enums/invoice-recurring-period.enum";
|
||||
import { InvoiceStatus } from "../enums/invoice-status.enum";
|
||||
@@ -10,7 +11,7 @@ import { InvoicesRepository } from "../repositories/invoices.repository";
|
||||
|
||||
@Entity({ repository: () => InvoicesRepository })
|
||||
export class Invoice extends BaseEntity {
|
||||
@Property({ type: "int", autoincrement: true, persist: false })
|
||||
@Property({ type: "int", autoincrement: true })
|
||||
numericId!: number & Opt;
|
||||
|
||||
@ManyToOne(() => Company, { nullable: false, deleteRule: "cascade" })
|
||||
@@ -40,7 +41,7 @@ export class Invoice extends BaseEntity {
|
||||
@Property({ type: "boolean", default: false })
|
||||
isRecurring: boolean & Opt;
|
||||
|
||||
@Enum({ items: () => RecurringPeriodEnum, nativeEnumName: "recurring_period", nullable: true })
|
||||
@Enum({ items: () => RecurringPeriodEnum, nullable: true, type: "smallint" })
|
||||
recurringPeriod?: RecurringPeriodEnum;
|
||||
|
||||
@Property({ type: "int", default: 0 })
|
||||
@@ -52,5 +53,8 @@ export class Invoice extends BaseEntity {
|
||||
@OneToMany(() => InvoiceItem, (item) => item.invoice, { cascade: [Cascade.PERSIST, Cascade.REMOVE] })
|
||||
items = new Collection<InvoiceItem>(this);
|
||||
|
||||
@ManyToOne(() => Business, { nullable: false, deleteRule: "cascade" })
|
||||
business!: Business;
|
||||
|
||||
[EntityRepositoryType]?: InvoicesRepository;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ export enum InvoiceStatus {
|
||||
PENDING = "PENDING",
|
||||
WAIT_PAYMENT = "WAIT_PAYMENT",
|
||||
PAID = "PAID",
|
||||
ARCHIVED = "ARCHIVED",
|
||||
OVERDUE = "OVERDUE",
|
||||
EXPIRED = "EXPIRED",
|
||||
CANCELLED = "CANCELLED",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query } from "@nestjs/common";
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common";
|
||||
import { ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { CreateInvoiceDto } from "./DTO/create-invoice.dto";
|
||||
@@ -6,56 +6,70 @@ import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "./DTO/invoic
|
||||
import { UpdateInvoiceDto } from "./DTO/update-invoice.dto";
|
||||
import { InvoicesService } from "./providers/invoices.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
||||
import { VerifyOtpWithUserId } from "../auth/DTO/verify-otp.dto";
|
||||
import { Business } from "../businesses/entities/business.entity";
|
||||
import { RoleEnum } from "../users/enums/role.enum";
|
||||
|
||||
@Controller("invoices")
|
||||
@UseInterceptors(BusinessInterceptor)
|
||||
@AuthGuards()
|
||||
export class InvoicesController {
|
||||
constructor(private readonly invoiceService: InvoicesService) {}
|
||||
|
||||
@ApiOperation({ summary: "create an invoice (admin)" })
|
||||
@Post()
|
||||
createInvoice(@Body() createDto: CreateInvoiceDto) {
|
||||
return this.invoiceService.createInvoiceAdmin(createDto);
|
||||
createInvoice(@Body() createDto: CreateInvoiceDto, @BusinessDec() business: Business) {
|
||||
return this.invoiceService.createInvoiceAdmin(createDto, business);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "update an invoice (admin)" })
|
||||
@Patch(":id")
|
||||
updateInvoice(@Param() paramDto: ParamDto, @Body() updateDto: UpdateInvoiceDto) {
|
||||
return this.invoiceService.updateInvoiceAdmin(paramDto.id, updateDto);
|
||||
updateInvoice(@Param() paramDto: ParamDto, @Body() updateDto: UpdateInvoiceDto, @BusinessDec("id") businessId: string) {
|
||||
return this.invoiceService.updateInvoiceAdmin(paramDto.id, updateDto, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "get all invoices (admin)" })
|
||||
@Get()
|
||||
getInvoices(@Query() queryDto: InvoicesSearchQueryDto) {
|
||||
return this.invoiceService.getInvoices(queryDto);
|
||||
getInvoices(@Query() queryDto: InvoicesSearchQueryDto, @BusinessDec("id") businessId: string) {
|
||||
return this.invoiceService.getInvoices(queryDto, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "get all user invoices" })
|
||||
@Get("user")
|
||||
getUserInvoices(@Query() queryDto: UserInvoicesSearchQueryDto, @UserDec("id") userId: string) {
|
||||
return this.invoiceService.getUserInvoices(queryDto, userId);
|
||||
getUserInvoices(@Query() queryDto: UserInvoicesSearchQueryDto, @UserDec("id") userId: string, @BusinessDec("id") businessId: string) {
|
||||
return this.invoiceService.getUserInvoices(queryDto, userId, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "get single invoice by Id " })
|
||||
@Get(":id")
|
||||
getInvoiceById(@Param() paramDto: ParamDto, @UserDec("role") role: RoleEnum, @UserDec("id") userId: string) {
|
||||
return this.invoiceService.getInvoiceById(paramDto.id, role, userId);
|
||||
getInvoiceById(
|
||||
@Param() paramDto: ParamDto,
|
||||
@UserDec("role") role: RoleEnum,
|
||||
@UserDec("id") userId: string,
|
||||
@BusinessDec("id") businessId: string,
|
||||
) {
|
||||
return this.invoiceService.getInvoiceById(paramDto.id, role, userId, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "approve request invoice by user" })
|
||||
@Post(":id/approve/request")
|
||||
approveRequestInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
return this.invoiceService.approveInvoiceRequest(paramDto.id, userId);
|
||||
approveRequestInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @BusinessDec("id") businessId: string) {
|
||||
return this.invoiceService.approveInvoiceRequest(paramDto.id, userId, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "approve invoice by user" })
|
||||
@Patch(":id/approve")
|
||||
approveInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @Body() verifyOtpDto: VerifyOtpWithUserId) {
|
||||
return this.invoiceService.approveInvoiceByUser(paramDto.id, userId, verifyOtpDto);
|
||||
approveInvoiceByUser(
|
||||
@Param() paramDto: ParamDto,
|
||||
@UserDec("id") userId: string,
|
||||
@Body() verifyOtpDto: VerifyOtpWithUserId,
|
||||
@BusinessDec("id") businessId: string,
|
||||
) {
|
||||
return this.invoiceService.approveInvoiceByUser(paramDto.id, userId, verifyOtpDto, businessId);
|
||||
}
|
||||
|
||||
// @ApiOperation({ summary: "pay invoice by user" })
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Invoice } from "./entities/invoice.entity";
|
||||
import { InvoicesController } from "./invoices.controller";
|
||||
import { InvoicesService } from "./providers/invoices.service";
|
||||
import { InvoiceProcessor } from "./queue/invoice.processor";
|
||||
import { BusinessesModule } from "../businesses/businesses.module";
|
||||
import { CompaniesModule } from "../companies/companies.module";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
@@ -21,6 +22,7 @@ import { UtilsModule } from "../utils/utils.module";
|
||||
UtilsModule,
|
||||
NotificationModule,
|
||||
CompaniesModule,
|
||||
BusinessesModule,
|
||||
],
|
||||
providers: [InvoicesService, InvoiceProcessor],
|
||||
controllers: [InvoicesController],
|
||||
|
||||
@@ -7,9 +7,10 @@ import Decimal from "decimal.js";
|
||||
|
||||
import { AuthMessage, InvoiceMessage } from "../../../common/enums/message.enum";
|
||||
import { VerifyOtpWithUserId } from "../../auth/DTO/verify-otp.dto";
|
||||
import { Business } from "../../businesses/entities/business.entity";
|
||||
import { Company } from "../../companies/entities/company.entity";
|
||||
import { CompaniesService } from "../../companies/services/companies.service";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { RoleEnum } from "../../users/enums/role.enum";
|
||||
import { UsersService } from "../../users/services/users.service";
|
||||
import { OTPService } from "../../utils/providers/otp.service";
|
||||
@@ -41,31 +42,7 @@ export class InvoicesService {
|
||||
|
||||
///********************************** */
|
||||
|
||||
private validateInvoiceItems(items: InvoiceItemDto[]): void {
|
||||
if (!items || items.length === 0) throw new BadRequestException(InvoiceMessage.ITEMS_REQUIRED);
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item.unitPrice <= 0) throw new BadRequestException(InvoiceMessage.UNIT_PRICE_MUST_BE_POSITIVE);
|
||||
if (item.count <= 0) throw new BadRequestException(InvoiceMessage.COUNT_MUST_BE_POSITIVE);
|
||||
if (item.discount && (item.discount < 0 || item.discount > 100)) {
|
||||
throw new BadRequestException(InvoiceMessage.DISCOUNT_MUST_BE_BETWEEN_0_AND_100);
|
||||
}
|
||||
});
|
||||
}
|
||||
///********************************** */
|
||||
|
||||
private validateRecurringInvoice(createDto: CreateInvoiceDto): void {
|
||||
if (createDto.isRecurring) {
|
||||
if (!createDto.recurringPeriod) throw new BadRequestException(InvoiceMessage.RECURRING_PERIOD_REQUIRED);
|
||||
|
||||
if (createDto.maxRecurringCycles && createDto.maxRecurringCycles < 1) {
|
||||
throw new BadRequestException(InvoiceMessage.MAX_RECURRING_CYCLES_MUST_BE_POSITIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
///********************************** */
|
||||
|
||||
async createInvoiceAdmin(createDto: CreateInvoiceDto) {
|
||||
async createInvoiceAdmin(createDto: CreateInvoiceDto, business: Business) {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
@@ -85,7 +62,7 @@ export class InvoicesService {
|
||||
return invoiceItem;
|
||||
});
|
||||
|
||||
const totalPrice = invoiceItems.reduce((sum, item) => new Decimal(item.totalPrice).add(sum), new Decimal(0));
|
||||
const totalPrice = invoiceItems.reduce((sum, item) => new Decimal(item.totalPrice).add(sum), new Decimal(0)).round();
|
||||
const tax = totalPrice.mul(0.1);
|
||||
|
||||
if (totalPrice.lessThanOrEqualTo(0)) throw new BadRequestException(InvoiceMessage.TOTAL_PRICE_MUST_BE_POSITIVE);
|
||||
@@ -106,6 +83,7 @@ export class InvoicesService {
|
||||
recurringPeriod: createDto.recurringPeriod,
|
||||
maxRecurringCycles: createDto.maxRecurringCycles,
|
||||
currentRecurringCycle: 0,
|
||||
business,
|
||||
});
|
||||
|
||||
await em.persistAndFlush(invoice);
|
||||
@@ -140,13 +118,13 @@ export class InvoicesService {
|
||||
|
||||
//********************************** */
|
||||
|
||||
async updateInvoiceAdmin(invoiceId: string, updateDto: UpdateInvoiceDto) {
|
||||
async updateInvoiceAdmin(invoiceId: string, updateDto: UpdateInvoiceDto, businessId: string) {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
await em.begin();
|
||||
|
||||
const invoice = await em.findOne(Invoice, { id: invoiceId }, { populate: ["items"] });
|
||||
const invoice = await em.findOne(Invoice, { id: invoiceId, business: { id: businessId } }, { populate: ["items"] });
|
||||
|
||||
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
||||
|
||||
@@ -170,7 +148,7 @@ export class InvoicesService {
|
||||
});
|
||||
|
||||
// Calculate total price and tax
|
||||
const totalPrice = invoiceItems.reduce((sum, item) => sum.add(item.totalPrice), new Decimal(0));
|
||||
const totalPrice = invoiceItems.reduce((sum, item) => sum.add(item.totalPrice), new Decimal(0)).round();
|
||||
const tax = totalPrice.mul(0.1);
|
||||
|
||||
invoice.totalPrice = totalPrice.add(tax);
|
||||
@@ -199,7 +177,7 @@ export class InvoicesService {
|
||||
}
|
||||
//********************************** */
|
||||
|
||||
async approveInvoiceRequest(invoiceId: string, userId: string) {
|
||||
async approveInvoiceRequest(invoiceId: string, userId: string, businessId: string) {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
@@ -207,7 +185,7 @@ export class InvoicesService {
|
||||
|
||||
const user = await this.usersService.findOneByIdWithEntityManager(userId, em);
|
||||
|
||||
const invoice = await this.validateInvoiceForApproval(invoiceId, userId, em);
|
||||
const invoice = await this.validateInvoiceForApproval(invoiceId, userId, businessId, em);
|
||||
|
||||
const existCode = await this.otpService.checkExistOtp(user.phone, "INVOICE_VERIFY");
|
||||
if (existCode) {
|
||||
@@ -243,7 +221,7 @@ export class InvoicesService {
|
||||
|
||||
//********************************** */
|
||||
|
||||
async approveInvoiceByUser(invoiceId: string, userId: string, verifyOtpDto: VerifyOtpWithUserId) {
|
||||
async approveInvoiceByUser(invoiceId: string, userId: string, verifyOtpDto: VerifyOtpWithUserId, businessId: string) {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
@@ -256,7 +234,7 @@ export class InvoicesService {
|
||||
const isValid = await this.otpService.verifyOtp(user.phone, code, "INVOICE_VERIFY");
|
||||
if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP);
|
||||
|
||||
const invoice = await this.validateInvoiceForApproval(invoiceId, userId, em);
|
||||
const invoice = await this.validateInvoiceForApproval(invoiceId, userId, businessId, em);
|
||||
|
||||
invoice.status = InvoiceStatus.WAIT_PAYMENT;
|
||||
await em.persistAndFlush(invoice);
|
||||
@@ -286,8 +264,12 @@ export class InvoicesService {
|
||||
}
|
||||
|
||||
//********************************** */
|
||||
private async validateInvoiceForApproval(invoiceId: string, userId: string, em: EntityManager): Promise<Invoice> {
|
||||
const invoice = await em.findOne(Invoice, { id: invoiceId, company: { user: { id: userId } } }, { populate: ["items"] });
|
||||
private async validateInvoiceForApproval(invoiceId: string, userId: string, businessId: string, em: EntityManager): Promise<Invoice> {
|
||||
const invoice = await em.findOne(
|
||||
Invoice,
|
||||
{ id: invoiceId, company: { user: { id: userId } }, business: { id: businessId } },
|
||||
{ populate: ["items"] },
|
||||
);
|
||||
|
||||
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
||||
|
||||
@@ -296,7 +278,7 @@ export class InvoicesService {
|
||||
if (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID);
|
||||
|
||||
if (dayjs().isAfter(dayjs(invoice.dueDate).add(INVOICE.MAX_DAYS_AFTER_OVERDUE, "day"))) {
|
||||
invoice.status = InvoiceStatus.EXPIRED;
|
||||
invoice.status = InvoiceStatus.ARCHIVED;
|
||||
await em.persistAndFlush(invoice);
|
||||
throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE);
|
||||
}
|
||||
@@ -308,21 +290,18 @@ export class InvoicesService {
|
||||
|
||||
///********************************** */
|
||||
|
||||
///********************************** */
|
||||
async getInvoices(queryDto: InvoicesSearchQueryDto, businessId: string) {
|
||||
const [invoices, count] = await this.invoiceRepository.getInvoicesForAdmin(queryDto, businessId);
|
||||
|
||||
async getInvoices(queryDto: InvoicesSearchQueryDto) {
|
||||
const [invoices, count] = await this.invoiceRepository.getInvoicesForAdmin(queryDto);
|
||||
|
||||
// Get unique users that have invoices using MikroORM's query builder
|
||||
const usersThatHaveInvoices = await this.em
|
||||
.createQueryBuilder(User, "user")
|
||||
.select(["user.id", "user.firstName", "user.lastName", "user.email", "user.phone"])
|
||||
const companiesThatHaveInvoices = await this.em
|
||||
.createQueryBuilder(Company, "company")
|
||||
.select(["company.id", "company.name"])
|
||||
.distinct()
|
||||
.join("user.invoices", "invoice")
|
||||
.join("company.invoices", "invoice")
|
||||
.getResult();
|
||||
|
||||
return {
|
||||
users: usersThatHaveInvoices,
|
||||
companies: companiesThatHaveInvoices,
|
||||
invoices,
|
||||
count,
|
||||
paginate: true,
|
||||
@@ -330,13 +309,19 @@ export class InvoicesService {
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
async getInvoiceById(invoiceId: string, role: RoleEnum, userId: string) {
|
||||
async getInvoiceById(invoiceId: string, role: RoleEnum, userId: string, businessId: string) {
|
||||
let invoice: Invoice | null;
|
||||
|
||||
if (role === RoleEnum.ADMIN) {
|
||||
invoice = await this.invoiceRepository.findOne(invoiceId, { populate: ["items", "company"] });
|
||||
if (role === RoleEnum.USER) {
|
||||
invoice = await this.invoiceRepository.findOne(
|
||||
{ id: invoiceId, company: { user: { id: userId } }, business: { id: businessId } },
|
||||
{ populate: ["items", "company"] },
|
||||
);
|
||||
} else {
|
||||
invoice = await this.invoiceRepository.findOne({ id: invoiceId, company: { user: { id: userId } } }, { populate: ["items"] });
|
||||
invoice = await this.invoiceRepository.findOne(
|
||||
{ id: invoiceId, business: { id: businessId } },
|
||||
{ populate: ["items", "company", "business"] },
|
||||
);
|
||||
}
|
||||
|
||||
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
||||
@@ -348,13 +333,13 @@ export class InvoicesService {
|
||||
|
||||
//*********************************** */
|
||||
|
||||
async getUserInvoices(queryDto: UserInvoicesSearchQueryDto, userId: string) {
|
||||
async getUserInvoices(queryDto: UserInvoicesSearchQueryDto, userId: string, businessId: string) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const qb = this.em
|
||||
.createQueryBuilder(Invoice, "invoice")
|
||||
.leftJoinAndSelect("invoice.items", "items")
|
||||
.where({ company: { user: { id: userId } } })
|
||||
.where({ company: { user: { id: userId } }, business: { id: businessId } })
|
||||
.orderBy({ createdAt: "DESC" });
|
||||
|
||||
if (queryDto.status) {
|
||||
@@ -553,4 +538,27 @@ export class InvoicesService {
|
||||
}
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
private validateInvoiceItems(items: InvoiceItemDto[]): void {
|
||||
if (!items || items.length === 0) throw new BadRequestException(InvoiceMessage.ITEMS_REQUIRED);
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item.unitPrice <= 0) throw new BadRequestException(InvoiceMessage.UNIT_PRICE_MUST_BE_POSITIVE);
|
||||
if (item.count <= 0) throw new BadRequestException(InvoiceMessage.COUNT_MUST_BE_POSITIVE);
|
||||
if (item.discount && (item.discount < 0 || item.discount > 100)) {
|
||||
throw new BadRequestException(InvoiceMessage.DISCOUNT_MUST_BE_BETWEEN_0_AND_100);
|
||||
}
|
||||
});
|
||||
}
|
||||
///********************************** */
|
||||
|
||||
private validateRecurringInvoice(createDto: CreateInvoiceDto): void {
|
||||
if (createDto.isRecurring) {
|
||||
if (!createDto.recurringPeriod) throw new BadRequestException(InvoiceMessage.RECURRING_PERIOD_REQUIRED);
|
||||
|
||||
if (createDto.maxRecurringCycles && createDto.maxRecurringCycles < 1) {
|
||||
throw new BadRequestException(InvoiceMessage.MAX_RECURRING_CYCLES_MUST_BE_POSITIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ export class InvoiceProcessor extends WorkerProcessor {
|
||||
}
|
||||
|
||||
async process(job: Job, token?: string) {
|
||||
this.logger.log(job);
|
||||
switch (job.name) {
|
||||
case INVOICE.REMINDER_JOB_NAME:
|
||||
return this.sendBillInvoiceReminder(job, token);
|
||||
@@ -65,7 +64,7 @@ export class InvoiceProcessor extends WorkerProcessor {
|
||||
throw new Error(`Maximum recurring cycles (${invoice.maxRecurringCycles}) reached for invoice ${invoice.id}`);
|
||||
}
|
||||
|
||||
if (invoice.status === InvoiceStatus.CANCELLED) throw new Error(`Cannot create recurring invoice for cancelled invoice ${invoice.id}`);
|
||||
if (invoice.status === InvoiceStatus.ARCHIVED) throw new Error(`Cannot create recurring invoice for archived invoice ${invoice.id}`);
|
||||
}
|
||||
|
||||
//********************************** */
|
||||
@@ -87,6 +86,7 @@ export class InvoiceProcessor extends WorkerProcessor {
|
||||
recurringPeriod: invoice.recurringPeriod,
|
||||
maxRecurringCycles: invoice.maxRecurringCycles,
|
||||
currentRecurringCycle: invoice.currentRecurringCycle + 1,
|
||||
business: invoice.business,
|
||||
});
|
||||
|
||||
// Update the original invoice's current recurring cycle
|
||||
@@ -149,10 +149,10 @@ export class InvoiceProcessor extends WorkerProcessor {
|
||||
//********************************** */
|
||||
|
||||
private async cancelOverdueInvoice(invoice: Invoice, em: EntityManager) {
|
||||
invoice.status = InvoiceStatus.CANCELLED;
|
||||
invoice.status = InvoiceStatus.ARCHIVED;
|
||||
await em.persistAndFlush(invoice);
|
||||
|
||||
this.logger.log(`Invoice ${invoice.id} has been cancelled as it is more than 10 days overdue.`);
|
||||
this.logger.log(`Invoice ${invoice.id} has been archived as it is more than 10 days overdue.`);
|
||||
}
|
||||
//********************************** */
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import { Invoice } from "../entities/invoice.entity";
|
||||
|
||||
export class InvoicesRepository extends EntityRepository<Invoice> {
|
||||
//
|
||||
async getInvoicesForAdmin(queryDto: InvoicesSearchQueryDto) {
|
||||
async getInvoicesForAdmin(queryDto: InvoicesSearchQueryDto, businessId: string) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const where: FilterQuery<Invoice> = {};
|
||||
const where: FilterQuery<Invoice> = { business: { id: businessId } };
|
||||
|
||||
if (queryDto.q) {
|
||||
where.$or = [
|
||||
@@ -25,7 +25,7 @@ export class InvoicesRepository extends EntityRepository<Invoice> {
|
||||
}
|
||||
|
||||
if (queryDto.companyId) {
|
||||
where.company = queryDto.companyId;
|
||||
where.company = { id: queryDto.companyId };
|
||||
}
|
||||
|
||||
if (queryDto.since || queryDto.to) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{title}}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.content {
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #6c757d;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h2>{{title}}</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{message}}</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This is an automated message from DanakCo. Please do not reply to this email.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>اعلان</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>اعلان</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<div style="font-size: 24px; color: #2c3e50; text-align: center; margin: 20px 0;">{{title}}</div>
|
||||
<div style="margin: 20px 0; text-align: center;">{{description}}</div>
|
||||
<div style="text-align: center; color: #666; font-size: 14px;">تاریخ: {{date}}</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{title}}</title>
|
||||
<style>
|
||||
body, p, a { margin: 0; padding: 0; font-family: Arial, sans-serif; } body { background-color: #f5f5f5; color: #333333; text-align:
|
||||
right; line-height: 1.6; } /* Container */ .container { width: 100%; max-width: 600px; margin: 40px auto; padding: 20px;
|
||||
background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } /* Logo */ .logo { text-align: center;
|
||||
margin-bottom: 20px; } .logo img { max-width: 50px; height: auto; } h1 { font-size: 24px; color: #1a1a1a; margin-bottom: 20px;
|
||||
text-align: center; } p { font-size: 16px; color: #4a4a4a; margin-bottom: 20px; } .btn { display: inline-block; padding: 12px 24px;
|
||||
margin: 20px auto; background-color: #030a11; color: #ffffff; text-decoration: none; border-radius: 6px; font-size: 16px; text-align:
|
||||
center; transition: background-color 0.3s ease; } .btn:hover { background-color: #6d7074; } a { color: #373b3f; text-decoration: none;
|
||||
} a:hover { text-decoration: none; } .footer { margin-top: 30px; font-size: 14px; color: #777777; text-align: center; } .footer a {
|
||||
color: #007bff; } .footer a:hover { text-decoration: underline; } /* Responsive Design */ @media (max-width: 600px) { .container {
|
||||
margin: 20px auto; padding: 15px; } h1 { font-size: 20px; } p { font-size: 14px; } .btn { font-size: 14px; padding: 10px 20px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<img src="https://danakcorp.com/favicon.png" alt="DanakCorp Logo" />
|
||||
</div>
|
||||
<p>سلام {{fullName}} عزیز,</p>
|
||||
|
||||
<p>به <strong>DanakCorp</strong> خوش آمدید! لطفاً برای تأیید ایمیل خود روی دکمه زیر کلیک کنید:</p>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{link}}" class="btn">تأیید ایمیل</a>
|
||||
</div>
|
||||
|
||||
<p>اگر دکمه بالا کار نمیکند، میتوانید از لینک زیر استفاده کنید:</p>
|
||||
<p style="word-break: break-all; text-align: center">
|
||||
<a href="{{link}}">{{link}}</a>
|
||||
</p>
|
||||
|
||||
<p style="color: #777777; font-size: 14px; text-align: center">
|
||||
اگر شما این درخواست را ارسال نکردهاید، لطفاً این ایمیل را نادیده بگیرید.
|
||||
</p>
|
||||
|
||||
<div class="footer">
|
||||
<p>با تشکر،<br />تیم پشتیبانی <strong>DanakCorp</strong></p>
|
||||
<p><a href="https://danakcorp.com">danakcorp.com</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>لغو آزمون</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>لغو آزمون</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>آزمون زیر با موفقیت لغو شد:</p>
|
||||
<div style="font-size: 20px; color: #2c3e50; text-align: center; margin: 20px 0;">{{examTitle}}</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>فاکتور</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>فاکتور</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>جزئیات فاکتور شما به شرح زیر است:</p>
|
||||
<div style="margin: 20px 0;">
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>مبلغ:</span>
|
||||
<span>{{price}} تومان</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>تاریخ ایجاد:</span>
|
||||
<span>{{createDate}}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>تاریخ سررسید:</span>
|
||||
<span>{{dueDate}}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>شناسه فاکتور:</span>
|
||||
<span>{{invoiceId}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ورود به سیستم</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>ورود به سیستم</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>ورود به سیستم در تاریخ و ساعت زیر انجام شد:</p>
|
||||
<div style="font-size: 20px; color: #2c3e50; text-align: center; margin: 20px 0;">{{date}}</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>لغو پرداخت</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>لغو پرداخت</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>پرداخت مبلغ زیر به دلیل عدم پرداخت در زمان مقرر لغو شد:</p>
|
||||
<div style="font-size: 24px; color: #e74c3c; text-align: center; margin: 20px 0;">{{amount}} تومان</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>یادآوری پرداخت</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>یادآوری پرداخت</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>لطفاً نسبت به پرداخت مبلغ زیر اقدام فرمایید:</p>
|
||||
<div style="font-size: 24px; color: #e74c3c; text-align: center; margin: 20px 0;">{{amount}} تومان</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,42 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>تیکت پشتیبانی</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>تیکت پشتیبانی</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>جزئیات تیکت پشتیبانی شما به شرح زیر است:</p>
|
||||
<div style="margin: 20px 0;">
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>موضوع:</span>
|
||||
<span>{{subject}}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>تاریخ:</span>
|
||||
<span>{{date}}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>شناسه تیکت:</span>
|
||||
<span>{{ticketId}}</span>
|
||||
</div>
|
||||
{{#if user}}
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>کارشناس:</span>
|
||||
<span>{{user}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>اعلان کیف پول</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>اعلان کیف پول</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>جزئیات تراکنش کیف پول شما به شرح زیر است:</p>
|
||||
<div style="margin: 20px 0;">
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>مبلغ تراکنش:</span>
|
||||
<div style="font-size: 24px; color: #2ecc71; text-align: center; margin: 20px 0;">{{amount}} تومان</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>موجودی فعلی:</span>
|
||||
<div style="font-size: 20px; color: #3498db; text-align: center; margin: 20px 0;">{{balance}} تومان</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>تاریخ:</span>
|
||||
<span>{{date}}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>دلیل:</span>
|
||||
<span>{{reason}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsNotEmpty, IsString, IsUUID, Length } from "class-validator";
|
||||
import { IsBoolean, IsNotEmpty, IsString, Length } from "class-validator";
|
||||
|
||||
import { TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||
|
||||
@@ -16,11 +16,6 @@ export class CreateTicketCategoryDto {
|
||||
@ApiProperty({ description: "Description of the ticket category", example: "درخواستهای فنی", minLength: 3, maxLength: 500 })
|
||||
description: string;
|
||||
|
||||
@IsNotEmpty({ message: TicketMessageEnum.USER_GROUP_ID_NOT_EMPTY })
|
||||
@IsUUID("4", { message: TicketMessageEnum.USER_GROUP_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "User group ID", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||
userGroupId: string;
|
||||
|
||||
@IsNotEmpty({ message: TicketMessageEnum.IS_ACTIVE_REQUIRED })
|
||||
@IsBoolean({ message: TicketMessageEnum.IS_ACTIVE_SHOULD_BE_BOOLEAN })
|
||||
@ApiProperty({ description: "Is active", example: true })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OmitType, PartialType } from "@nestjs/swagger";
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateTicketCategoryDto } from "./create-ticket-category.dto";
|
||||
|
||||
export class UpdateTicketCategoryDto extends PartialType(OmitType(CreateTicketCategoryDto, ["userGroupId"])) {}
|
||||
export class UpdateTicketCategoryDto extends PartialType(CreateTicketCategoryDto) {}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Collection, Entity, OneToMany, Property } from "@mikro-orm/core";
|
||||
import { Collection, Entity, EntityRepositoryType, Formula, ManyToOne, OneToMany, Property } from "@mikro-orm/core";
|
||||
|
||||
import { Ticket } from "./ticket.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { Business } from "../../businesses/entities/business.entity";
|
||||
import { TicketCategoryRepository } from "../repositories/tickets-category.repository";
|
||||
|
||||
@Entity()
|
||||
@Entity({ repository: () => TicketCategoryRepository })
|
||||
export class TicketCategory extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 150, unique: true })
|
||||
title!: string;
|
||||
@@ -14,6 +16,14 @@ export class TicketCategory extends BaseEntity {
|
||||
@Property({ type: "boolean", default: true })
|
||||
isActive: boolean = true;
|
||||
|
||||
@Formula((alias) => `(SELECT COUNT(*)::int FROM ticket WHERE ticket.category_id = ${alias}.id)`, { persist: false })
|
||||
ticketCount?: number;
|
||||
|
||||
@OneToMany(() => Ticket, (ticket) => ticket.category)
|
||||
tickets = new Collection<Ticket>(this);
|
||||
|
||||
@ManyToOne(() => Business, { deleteRule: "cascade", nullable: false })
|
||||
business!: Business;
|
||||
|
||||
[EntityRepositoryType]?: TicketCategoryRepository;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Entity, ManyToOne, Property } from "@mikro-orm/core";
|
||||
import { Entity, EntityRepositoryType, ManyToOne, Property } from "@mikro-orm/core";
|
||||
|
||||
import { TicketMessage } from "./ticket-message.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { TicketMessageAttachmentsRepository } from "../repositories/ticket-message-attachment.repository";
|
||||
|
||||
@Entity()
|
||||
@Entity({ repository: () => TicketMessageAttachmentsRepository })
|
||||
export class TicketMessageAttachment extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 150, nullable: true })
|
||||
attachmentUrl?: string;
|
||||
|
||||
@ManyToOne({ entity: () => TicketMessage, deleteRule: "cascade", nullable: false })
|
||||
ticketMessage!: TicketMessage;
|
||||
|
||||
[EntityRepositoryType]?: TicketMessageAttachmentsRepository;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Cascade, Collection, Entity, ManyToOne, OneToMany, Property } from "@mikro-orm/core";
|
||||
import { Cascade, Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, Property } from "@mikro-orm/core";
|
||||
|
||||
import { TicketMessageAttachment } from "./ticket-message-attachment";
|
||||
import { Ticket } from "./ticket.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { TicketMessagesRepository } from "../repositories/tickets-message.repository";
|
||||
|
||||
@Entity()
|
||||
@Entity({ repository: () => TicketMessagesRepository })
|
||||
export class TicketMessage extends BaseEntity {
|
||||
@Property({ type: "text" })
|
||||
content!: string;
|
||||
@@ -18,4 +19,6 @@ export class TicketMessage extends BaseEntity {
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "cascade", nullable: false })
|
||||
author!: User;
|
||||
|
||||
[EntityRepositoryType]?: TicketMessagesRepository;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Cascade, Collection, Entity, Enum, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core";
|
||||
import { Cascade, Collection, Entity, EntityRepositoryType, Enum, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { TicketCategory } from "./ticket-category.entity";
|
||||
import { TicketMessage } from "./ticket-message.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { Business } from "../../businesses/entities/business.entity";
|
||||
import { Company } from "../../companies/entities/company.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { TicketPriority } from "../enums/ticket-priority.enum";
|
||||
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||
import { TicketsRepository } from "../repositories/tickets.repository";
|
||||
|
||||
@Entity()
|
||||
@Entity({ repository: () => TicketsRepository })
|
||||
export class Ticket extends BaseEntity {
|
||||
@Property({ type: "int", autoincrement: true, persist: false })
|
||||
numericId!: number & Opt;
|
||||
@@ -34,6 +36,11 @@ export class Ticket extends BaseEntity {
|
||||
@ManyToOne(() => TicketCategory, { deleteRule: "set null", nullable: true })
|
||||
category?: TicketCategory;
|
||||
|
||||
@ManyToOne(() => Business, { deleteRule: "cascade", nullable: false })
|
||||
business!: Business;
|
||||
|
||||
@OneToMany(() => TicketMessage, (message) => message.ticket, { cascade: [Cascade.ALL] })
|
||||
messages = new Collection<TicketMessage>(this);
|
||||
|
||||
[EntityRepositoryType]?: TicketsRepository;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BadRequestException, HttpException, Injectable, InternalServerErrorExce
|
||||
|
||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||
import { CommonMessage, CompanyMessage, TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { Business } from "../../businesses/entities/business.entity";
|
||||
import { Company } from "../../companies/entities/company.entity";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
@@ -38,26 +39,27 @@ export class TicketsService {
|
||||
) {}
|
||||
|
||||
//******************************** */
|
||||
async getTicketCategories() {
|
||||
const ticketCategories = await this.ticketsCategoryRepository.find({});
|
||||
async getTicketCategories(businessId: string) {
|
||||
const ticketCategories = await this.ticketsCategoryRepository.find({ business: { id: businessId } });
|
||||
return {
|
||||
ticketCategories,
|
||||
};
|
||||
}
|
||||
//******************************** */
|
||||
|
||||
async getCategoriesList(queryDto: SearchTicketCategoryDto) {
|
||||
const [categories, count] = await this.ticketsCategoryRepository.findCategoriesList(queryDto);
|
||||
async getCategoriesList(queryDto: SearchTicketCategoryDto, businessId: string) {
|
||||
const [categories, count] = await this.ticketsCategoryRepository.findCategoriesList(queryDto, businessId);
|
||||
|
||||
return { categories, count, paginate: true };
|
||||
}
|
||||
//******************************** */
|
||||
async createTicketCategory(createDto: CreateTicketCategoryDto) {
|
||||
const existCategory = await this.ticketsCategoryRepository.findCategoryByTitle(createDto.title);
|
||||
async createTicketCategory(createDto: CreateTicketCategoryDto, business: Business) {
|
||||
const existCategory = await this.ticketsCategoryRepository.findCategoryByTitle(createDto.title, business.id);
|
||||
|
||||
if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST);
|
||||
const ticketCategory = this.ticketsCategoryRepository.create({
|
||||
...createDto,
|
||||
business,
|
||||
});
|
||||
|
||||
await this.em.persistAndFlush(ticketCategory);
|
||||
@@ -69,8 +71,8 @@ export class TicketsService {
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
async updateCategory(paramDto: ParamDto, updateCategoryDto: UpdateTicketCategoryDto) {
|
||||
const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id);
|
||||
async updateCategory(paramDto: ParamDto, updateCategoryDto: UpdateTicketCategoryDto, businessId: string) {
|
||||
const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id, businessId);
|
||||
if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
||||
|
||||
if (updateCategoryDto.title) {
|
||||
@@ -88,9 +90,25 @@ export class TicketsService {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
};
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
async toggleCategoryStatus(paramDto: ParamDto) {
|
||||
const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id);
|
||||
async deleteCategory(paramDto: ParamDto, businessId: string) {
|
||||
const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id, businessId);
|
||||
if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
||||
|
||||
if (category.tickets.length > 0) throw new BadRequestException(TicketMessageEnum.CATEGORY_HAS_TICKETS);
|
||||
|
||||
category.deletedAt = new Date();
|
||||
await this.em.flush();
|
||||
|
||||
return {
|
||||
message: TicketMessageEnum.CATEGORY_DELETED,
|
||||
};
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
async toggleCategoryStatus(paramDto: ParamDto, businessId: string) {
|
||||
const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id, businessId);
|
||||
if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
||||
//
|
||||
category.isActive = !category.isActive;
|
||||
@@ -104,7 +122,7 @@ export class TicketsService {
|
||||
|
||||
//******************************** */
|
||||
|
||||
async createTicket(createDto: CreateTicketDto, userId: string) {
|
||||
async createTicket(createDto: CreateTicketDto, userId: string, business: Business) {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
@@ -142,6 +160,7 @@ export class TicketsService {
|
||||
category: ticketCategory,
|
||||
messages: [ticketMessage],
|
||||
company,
|
||||
business,
|
||||
});
|
||||
|
||||
await em.persistAndFlush(ticket);
|
||||
@@ -163,27 +182,27 @@ export class TicketsService {
|
||||
|
||||
//******************************** */
|
||||
|
||||
async getTickets(queryDto: SearchTicketQueryDto, userId: string) {
|
||||
const [tickets, count] = await this.ticketsRepository.findTicketsList(queryDto, userId);
|
||||
async getTickets(queryDto: SearchTicketQueryDto, userId: string, businessId: string) {
|
||||
const [tickets, count] = await this.ticketsRepository.findTicketsList(queryDto, userId, businessId);
|
||||
|
||||
return { tickets, count, paginate: true };
|
||||
}
|
||||
//******************************** */
|
||||
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string) {
|
||||
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId);
|
||||
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string, businessId: string) {
|
||||
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId, businessId);
|
||||
|
||||
return { tickets, count, paginate: true };
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
|
||||
async createTicketMessage(ticketId: string, createDto: CreateTicketMessageDto, userId: string, role: RoleEnum) {
|
||||
async createTicketMessage(ticketId: string, createDto: CreateTicketMessageDto, userId: string, role: RoleEnum, businessId: string) {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
await em.begin();
|
||||
|
||||
const ticket = await this.findTicket(em, ticketId, userId, role === RoleEnum.ADMIN);
|
||||
const ticket = await this.findTicket(em, ticketId, businessId, userId, role === RoleEnum.ADMIN);
|
||||
if (ticket.status === TicketStatus.CLOSED) throw new BadRequestException(TicketMessageEnum.TICKET_CLOSED);
|
||||
|
||||
if (role === RoleEnum.ADMIN && ticket.assignedTo?.id !== userId)
|
||||
@@ -224,13 +243,13 @@ export class TicketsService {
|
||||
|
||||
//******************************** */
|
||||
|
||||
async getTicketMessages(ticketId: string, userId: string, role: RoleEnum) {
|
||||
async getTicketMessages(ticketId: string, userId: string, role: RoleEnum, businessId: string) {
|
||||
let ticket = null;
|
||||
//
|
||||
if (role === RoleEnum.ADMIN) {
|
||||
ticket = await this.ticketsRepository.findTicketById(ticketId);
|
||||
ticket = await this.ticketsRepository.findTicketById(ticketId, businessId);
|
||||
} else {
|
||||
ticket = await this.ticketsRepository.findTicketById(ticketId, userId);
|
||||
ticket = await this.ticketsRepository.findTicketById(ticketId, businessId, userId);
|
||||
}
|
||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||
|
||||
@@ -243,13 +262,13 @@ export class TicketsService {
|
||||
|
||||
//******************************** */
|
||||
|
||||
async closeTicketByUser(ticketId: string, userId: string, role: RoleEnum) {
|
||||
async closeTicketByUser(ticketId: string, userId: string, role: RoleEnum, businessId: string) {
|
||||
let ticket = null;
|
||||
|
||||
if (role === RoleEnum.ADMIN) {
|
||||
ticket = await this.ticketsRepository.findTicketById(ticketId);
|
||||
ticket = await this.ticketsRepository.findTicketById(ticketId, businessId);
|
||||
} else {
|
||||
ticket = await this.ticketsRepository.findTicketById(ticketId, userId);
|
||||
ticket = await this.ticketsRepository.findTicketById(ticketId, businessId, userId);
|
||||
}
|
||||
|
||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||
@@ -262,8 +281,8 @@ export class TicketsService {
|
||||
};
|
||||
}
|
||||
//******************************** * /
|
||||
async openTicketByAdmin(ticketId: string) {
|
||||
const ticket = await this.ticketsRepository.findTicketById(ticketId);
|
||||
async openTicketByAdmin(ticketId: string, businessId: string) {
|
||||
const ticket = await this.ticketsRepository.findTicketById(ticketId, businessId);
|
||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||
|
||||
ticket.status = TicketStatus.PENDING;
|
||||
@@ -275,14 +294,14 @@ export class TicketsService {
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
async referTicket(ticketId: string, userId: string, referDto: ReferTicketDto) {
|
||||
async referTicket(ticketId: string, userId: string, referDto: ReferTicketDto, businessId: string) {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
await em.begin();
|
||||
console.log(userId, "admin id");
|
||||
|
||||
const ticket = await this.ticketsRepository.findTicketById(ticketId);
|
||||
const ticket = await this.ticketsRepository.findTicketById(ticketId, businessId);
|
||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||
|
||||
const admin = await this.usersService.findOneByIdWithEntityManager(referDto.adminId, em);
|
||||
@@ -357,10 +376,14 @@ export class TicketsService {
|
||||
}
|
||||
//***************************** */
|
||||
|
||||
private async findTicket(em: EntityManager, ticketId: string, userId: string, isAdmin: boolean): Promise<Ticket> {
|
||||
private async findTicket(em: EntityManager, ticketId: string, businessId: string, userId: string, isAdmin: boolean): Promise<Ticket> {
|
||||
const ticket = isAdmin
|
||||
? await em.findOne(Ticket, { id: ticketId, user: { id: userId } }, { populate: ["user", "assignedTo"] })
|
||||
: await em.findOne(Ticket, { id: ticketId, user: { id: userId } }, { populate: ["user", "assignedTo"] });
|
||||
? await em.findOne(Ticket, { id: ticketId, user: { id: userId }, business: { id: businessId } }, { populate: ["user", "assignedTo"] })
|
||||
: await em.findOne(
|
||||
Ticket,
|
||||
{ id: ticketId, user: { id: userId }, business: { id: businessId } },
|
||||
{ populate: ["user", "assignedTo"] },
|
||||
);
|
||||
|
||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||
return ticket;
|
||||
|
||||
@@ -6,41 +6,18 @@ import { TicketCategory } from "../entities/ticket-category.entity";
|
||||
|
||||
export class TicketCategoryRepository extends EntityRepository<TicketCategory> {
|
||||
//
|
||||
async findCategoryByTitle(title: string): Promise<TicketCategory | null> {
|
||||
return this.findOne({ title });
|
||||
async findCategoryByTitle(title: string, businessId: string): Promise<TicketCategory | null> {
|
||||
return this.findOne({ title, business: { id: businessId }, deletedAt: null }, { populate: ["tickets"] });
|
||||
}
|
||||
|
||||
async findCategoryById(id: string): Promise<TicketCategory | null> {
|
||||
return this.findOne({ id });
|
||||
async findCategoryById(id: string, businessId: string): Promise<TicketCategory | null> {
|
||||
return this.findOne({ id, business: { id: businessId }, deletedAt: null }, { populate: ["tickets"] });
|
||||
}
|
||||
|
||||
async findCategoriesList(queryDto: SearchTicketCategoryDto) {
|
||||
async findCategoriesList(queryDto: SearchTicketCategoryDto, businessId: string) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const qb = this.createQueryBuilder("category")
|
||||
.leftJoinAndSelect("category.group", "group")
|
||||
.select(["category.*", "group.id", "group.name"]);
|
||||
|
||||
// Add ticket count
|
||||
qb.addSelect(
|
||||
this.em
|
||||
.createQueryBuilder(TicketCategory, "c")
|
||||
.count("t.id")
|
||||
.leftJoin("c.tickets", "t")
|
||||
.where("c.id = category.id")
|
||||
.as("ticketsCount"),
|
||||
);
|
||||
|
||||
// Add users count
|
||||
qb.addSelect(
|
||||
this.em
|
||||
.createQueryBuilder(TicketCategory, "c")
|
||||
.count("u.id")
|
||||
.leftJoin("c.group", "g")
|
||||
.leftJoin("g.users", "u")
|
||||
.where("c.id = category.id")
|
||||
.as("usersCount"),
|
||||
);
|
||||
const qb = this.createQueryBuilder("category").where({ business: { id: businessId }, deletedAt: null });
|
||||
|
||||
if (queryDto.isActive !== undefined) {
|
||||
qb.andWhere({ isActive: queryDto.isActive });
|
||||
|
||||
@@ -6,11 +6,12 @@ import { Ticket } from "../entities/ticket.entity";
|
||||
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||
|
||||
export class TicketsRepository extends EntityRepository<Ticket> {
|
||||
async findTicketById(id: string, userId?: string) {
|
||||
async findTicketById(id: string, businessId: string, userId?: string) {
|
||||
return this.findOne(
|
||||
{
|
||||
id,
|
||||
...(userId && { user: { id: userId } }),
|
||||
business: { id: businessId },
|
||||
},
|
||||
{
|
||||
populate: ["user", "category", "assignedTo", "company"],
|
||||
@@ -37,10 +38,10 @@ export class TicketsRepository extends EntityRepository<Ticket> {
|
||||
}
|
||||
//******************************** */
|
||||
|
||||
async findTicketsList(queryDto: SearchTicketQueryDto, userId: string) {
|
||||
async findTicketsList(queryDto: SearchTicketQueryDto, userId: string, businessId: string) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const where: FilterQuery<Ticket> = { user: { id: userId } };
|
||||
const where: FilterQuery<Ticket> = { user: { id: userId }, business: { id: businessId } };
|
||||
if (queryDto.status) {
|
||||
where.status = queryDto.status;
|
||||
} else {
|
||||
@@ -68,10 +69,10 @@ export class TicketsRepository extends EntityRepository<Ticket> {
|
||||
});
|
||||
}
|
||||
//******************************** */
|
||||
async findTicketsListForAdmin(queryDto: SearchTicketQueryDto, adminId: string) {
|
||||
async findTicketsListForAdmin(queryDto: SearchTicketQueryDto, adminId: string, businessId: string) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const where: FilterQuery<Ticket> = {};
|
||||
const where: FilterQuery<Ticket> = { business: { id: businessId } };
|
||||
|
||||
where.assignedTo = { id: adminId };
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common";
|
||||
import { ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { CreateTicketCategoryDto } from "./DTO/create-ticket-category.dto";
|
||||
import { CreateTicketMessageDto } from "./DTO/create-ticket-message.dto";
|
||||
@@ -11,71 +11,79 @@ import { UpdateTicketCategoryDto } from "./DTO/update-ticket-category.dto";
|
||||
import { TicketsService } from "./providers/tickets.service";
|
||||
import { AdminRoute } from "../../common/decorators/admin.decorator";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
||||
import { Business } from "../businesses/entities/business.entity";
|
||||
import { RoleEnum } from "../users/enums/role.enum";
|
||||
|
||||
@Controller("tickets")
|
||||
@ApiTags("Tickets")
|
||||
@UseInterceptors(BusinessInterceptor)
|
||||
@AuthGuards()
|
||||
export class TicketsController {
|
||||
constructor(private readonly ticketsService: TicketsService) {}
|
||||
|
||||
//----------------- ticket categories -------------------
|
||||
|
||||
@ApiOperation({ summary: "Create ticket category => admin route" })
|
||||
@ApiOperation({ summary: "Create ticket category (admin)" })
|
||||
@Post("category")
|
||||
createTicketCategory(@Body() createDto: CreateTicketCategoryDto) {
|
||||
return this.ticketsService.createTicketCategory(createDto);
|
||||
createTicketCategory(@Body() createDto: CreateTicketCategoryDto, @BusinessDec() business: Business) {
|
||||
return this.ticketsService.createTicketCategory(createDto, business);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Update ticket category => admin route" })
|
||||
@ApiOperation({ summary: "Update ticket category (admin)" })
|
||||
@Patch("category/:id")
|
||||
updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateTicketCategoryDto) {
|
||||
return this.ticketsService.updateCategory(paramDto, updateCategoryDto);
|
||||
updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateTicketCategoryDto, @BusinessDec("id") businessId: string) {
|
||||
return this.ticketsService.updateCategory(paramDto, updateCategoryDto, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Delete ticket category (admin)" })
|
||||
@Delete("category/:id")
|
||||
deleteCategory(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||
return this.ticketsService.deleteCategory(paramDto, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get ticket categories " })
|
||||
@Get("categories")
|
||||
//
|
||||
getTicketCategories() {
|
||||
return this.ticketsService.getTicketCategories();
|
||||
getTicketCategories(@BusinessDec("id") businessId: string) {
|
||||
return this.ticketsService.getTicketCategories(businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get ticket categories list => admin route" })
|
||||
@ApiOperation({ summary: "Get ticket categories list (admin)" })
|
||||
@AdminRoute()
|
||||
@Get("categories-list")
|
||||
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) {
|
||||
return this.ticketsService.getCategoriesList(queryDto);
|
||||
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto, @BusinessDec("id") businessId: string) {
|
||||
return this.ticketsService.getCategoriesList(queryDto, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "toggle status of categories => admin route" })
|
||||
@ApiOperation({ summary: "toggle status of categories (admin)" })
|
||||
@AdminRoute()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("category/toggle-status/:id")
|
||||
toggleCategoryStatus(@Param() paramDto: ParamDto) {
|
||||
return this.ticketsService.toggleCategoryStatus(paramDto);
|
||||
toggleCategoryStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||
return this.ticketsService.toggleCategoryStatus(paramDto, businessId);
|
||||
}
|
||||
|
||||
//----------------- ticket and ticket messages -------------------
|
||||
|
||||
@ApiOperation({ summary: "create ticket ==> user route" })
|
||||
@ApiOperation({ summary: "create ticket (user)" })
|
||||
@Post()
|
||||
createTicket(@Body() createDto: CreateTicketDto, @UserDec("id") userId: string) {
|
||||
return this.ticketsService.createTicket(createDto, userId);
|
||||
createTicket(@Body() createDto: CreateTicketDto, @UserDec("id") userId: string, @BusinessDec() business: Business) {
|
||||
return this.ticketsService.createTicket(createDto, userId, business);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all tickets of user ==> user route" })
|
||||
@ApiOperation({ summary: "Get all tickets of user (user)" })
|
||||
@Get()
|
||||
getTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") userId: string) {
|
||||
return this.ticketsService.getTickets(queryDto, userId);
|
||||
getTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") userId: string, @BusinessDec("id") businessId: string) {
|
||||
return this.ticketsService.getTickets(queryDto, userId, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all tickets ==> admin route" })
|
||||
@ApiOperation({ summary: "Get all tickets (admin)" })
|
||||
@AdminRoute()
|
||||
@Get("admin")
|
||||
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string) {
|
||||
return this.ticketsService.getTicketForAdmin(queryDto, adminId);
|
||||
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string, @BusinessDec("id") businessId: string) {
|
||||
return this.ticketsService.getTicketForAdmin(queryDto, adminId, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "create ticket messages" })
|
||||
@@ -85,34 +93,50 @@ export class TicketsController {
|
||||
@Body() createDto: CreateTicketMessageDto,
|
||||
@UserDec("id") userId: string,
|
||||
@UserDec("role") role: RoleEnum,
|
||||
@BusinessDec("id") businessId: string,
|
||||
) {
|
||||
return this.ticketsService.createTicketMessage(paramDto.id, createDto, userId, role);
|
||||
return this.ticketsService.createTicketMessage(paramDto.id, createDto, userId, role, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all ticket messages of user" })
|
||||
@Get(":id/messages")
|
||||
getTicketMessages(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @UserDec("role") role: RoleEnum) {
|
||||
return this.ticketsService.getTicketMessages(paramDto.id, userId, role);
|
||||
getTicketMessages(
|
||||
@Param() paramDto: ParamDto,
|
||||
@UserDec("id") userId: string,
|
||||
@UserDec("role") role: RoleEnum,
|
||||
@BusinessDec("id") businessId: string,
|
||||
) {
|
||||
return this.ticketsService.getTicketMessages(paramDto.id, userId, role, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "close ticket by user or admin" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post(":id/close")
|
||||
closedTicketByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @UserDec("role") role: RoleEnum) {
|
||||
return this.ticketsService.closeTicketByUser(paramDto.id, userId, role);
|
||||
closedTicketByUser(
|
||||
@Param() paramDto: ParamDto,
|
||||
@UserDec("id") userId: string,
|
||||
@UserDec("role") role: RoleEnum,
|
||||
@BusinessDec("id") businessId: string,
|
||||
) {
|
||||
return this.ticketsService.closeTicketByUser(paramDto.id, userId, role, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "open ticket by admin" })
|
||||
@AdminRoute()
|
||||
@Post(":id/open")
|
||||
openTicketByAdmin(@Param() paramDto: ParamDto) {
|
||||
return this.ticketsService.openTicketByAdmin(paramDto.id);
|
||||
openTicketByAdmin(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||
return this.ticketsService.openTicketByAdmin(paramDto.id, businessId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "referer ticket ==> admin route" })
|
||||
@AdminRoute()
|
||||
@Post(":id/refer")
|
||||
referTicket(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @Body() referDto: ReferTicketDto) {
|
||||
return this.ticketsService.referTicket(paramDto.id, userId, referDto);
|
||||
referTicket(
|
||||
@Param() paramDto: ParamDto,
|
||||
@UserDec("id") userId: string,
|
||||
@Body() referDto: ReferTicketDto,
|
||||
@BusinessDec("id") businessId: string,
|
||||
) {
|
||||
return this.ticketsService.referTicket(paramDto.id, userId, referDto, businessId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,10 @@ import { TicketCategory } from "./entities/ticket-category.entity";
|
||||
import { TicketMessage } from "./entities/ticket-message.entity";
|
||||
import { Ticket } from "./entities/ticket.entity";
|
||||
import { TicketsService } from "./providers/tickets.service";
|
||||
import { TicketCategoryRepository } from "./repositories/tickets-category.repository";
|
||||
import { TicketMessagesRepository } from "./repositories/tickets-message.repository";
|
||||
import { TicketsRepository } from "./repositories/tickets.repository";
|
||||
import { TicketsController } from "./tickets.controller";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { TicketMessageAttachment } from "./entities/ticket-message-attachment";
|
||||
import { TicketMessageAttachmentsRepository } from "./repositories/ticket-message-attachment.repository";
|
||||
import { BusinessesModule } from "../businesses/businesses.module";
|
||||
import { CompaniesModule } from "../companies/companies.module";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
|
||||
@@ -21,8 +18,9 @@ import { NotificationModule } from "../notifications/notifications.module";
|
||||
UsersModule,
|
||||
NotificationModule,
|
||||
CompaniesModule,
|
||||
BusinessesModule,
|
||||
],
|
||||
providers: [TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository, TicketMessageAttachmentsRepository],
|
||||
providers: [TicketsService],
|
||||
controllers: [TicketsController],
|
||||
exports: [TicketsService],
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, Opt, Pr
|
||||
import { RefreshToken } from "./refresh-token.entity";
|
||||
import { Role } from "./role.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { UserAnnouncement } from "../../announcements/entities/user-announcement.entity";
|
||||
import { Company } from "../../companies/entities/company.entity";
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
|
||||
@@ -56,5 +57,8 @@ export class User extends BaseEntity {
|
||||
@OneToMany(() => Company, (company) => company.user)
|
||||
companies = new Collection<Company>(this);
|
||||
|
||||
@OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.user)
|
||||
userAnnouncements = new Collection<UserAnnouncement>(this);
|
||||
|
||||
[EntityRepositoryType]?: UserRepository;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,20 @@ export function numberFormat(number: number, locale: string = "fa-IR") {
|
||||
return Intl.NumberFormat(locale).format(number);
|
||||
}
|
||||
|
||||
export function dateFormat(date: Date, locale: string = "fa-IR") {
|
||||
return new Intl.DateTimeFormat(locale).format(date);
|
||||
export function dateFormat(date: Date | string | number, locale: string = "fa-IR") {
|
||||
try {
|
||||
// Convert to Date object if it's a string or number
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
|
||||
// Check if the date is valid
|
||||
if (isNaN(dateObj.getTime())) {
|
||||
console.warn("Invalid date provided to dateFormat:", date);
|
||||
return "Invalid Date";
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(locale).format(dateObj);
|
||||
} catch (error) {
|
||||
console.error("Error formatting date:", error);
|
||||
return "Invalid Date";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user