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 { databaseConfig } from "./configs/mikro-orm.config";
|
||||||
import { rateLimitConfig } from "./configs/rateLimit.config";
|
import { rateLimitConfig } from "./configs/rateLimit.config";
|
||||||
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
||||||
|
import { AnnouncementsModule } from "./modules/announcements/announcement.module";
|
||||||
import { AuthModule } from "./modules/auth/auth.module";
|
import { AuthModule } from "./modules/auth/auth.module";
|
||||||
import { BusinessesModule } from "./modules/businesses/businesses.module";
|
import { BusinessesModule } from "./modules/businesses/businesses.module";
|
||||||
import { CompaniesModule } from "./modules/companies/companies.module";
|
import { CompaniesModule } from "./modules/companies/companies.module";
|
||||||
@@ -53,6 +54,7 @@ import { UtilsModule } from "./modules/utils/utils.module";
|
|||||||
TicketsModule,
|
TicketsModule,
|
||||||
InvoicesModule,
|
InvoicesModule,
|
||||||
BusinessesModule,
|
BusinessesModule,
|
||||||
|
AnnouncementsModule,
|
||||||
],
|
],
|
||||||
// providers: [
|
// providers: [
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -3,4 +3,6 @@ export const AUTH_THROTTLE_TTL = 5 * 60 * 1000;
|
|||||||
export const AUTH_THROTTLE_LIMIT = 5;
|
export const AUTH_THROTTLE_LIMIT = 5;
|
||||||
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
||||||
export const AUTH__REFRESH_THROTTLE_LIMIT = 10;
|
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 = "تاریخ انتشار نمیتواند در گذشته باشد",
|
PUBLISH_AT_CANNOT_BE_IN_PAST = "تاریخ انتشار نمیتواند در گذشته باشد",
|
||||||
USER_ANNOUNCEMENTS_DELETED = "اطلاعیههای کاربر با موفقیت حذف شدند",
|
USER_ANNOUNCEMENTS_DELETED = "اطلاعیههای کاربر با موفقیت حذف شدند",
|
||||||
QUEUE_JOBS_DELETED = "وظایف صف با موفقیت حذف شدند",
|
QUEUE_JOBS_DELETED = "وظایف صف با موفقیت حذف شدند",
|
||||||
|
COMPANY_MUST_BE_ID = "شناسه شرکت اجباری است",
|
||||||
|
COMPANY_MUST_BE_UUID = "شناسه شرکت باید یک UUID معتبر باشد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum CriticismMessage {
|
export const enum CriticismMessage {
|
||||||
@@ -293,6 +295,8 @@ export const enum TicketMessageEnum {
|
|||||||
INVALID_TICKET_LIMIT = "محدودیت تعداد تیکت در پلن پشتیبانی شما نامعتبر است",
|
INVALID_TICKET_LIMIT = "محدودیت تعداد تیکت در پلن پشتیبانی شما نامعتبر است",
|
||||||
TICKET_LIMIT_EXCEEDED = "شما به محدودیت تعداد تیکت در پلن پشتیبانی خود رسیدهاید",
|
TICKET_LIMIT_EXCEEDED = "شما به محدودیت تعداد تیکت در پلن پشتیبانی خود رسیدهاید",
|
||||||
OPENED = "تیکت با موفقیت باز شد",
|
OPENED = "تیکت با موفقیت باز شد",
|
||||||
|
CATEGORY_HAS_TICKETS = "دسته بندی دارای تیکت می باشد",
|
||||||
|
CATEGORY_DELETED = "دسته بندی با موفقیت حذف شد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum WalletMessage {
|
export const enum WalletMessage {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function mailerConfig(): MailerAsyncOptions {
|
|||||||
},
|
},
|
||||||
|
|
||||||
template: {
|
template: {
|
||||||
dir: process.cwd() + "/src/templates/email",
|
dir: process.cwd() + "/src/modules/templates/email",
|
||||||
adapter: new HandlebarsAdapter(),
|
adapter: new HandlebarsAdapter(),
|
||||||
options: {
|
options: {
|
||||||
strict: true,
|
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 { UtilsModule } from "../utils/utils.module";
|
||||||
import { AuthService } from "./providers/auth.service";
|
import { AuthService } from "./providers/auth.service";
|
||||||
import { TokensService } from "./providers/tokens.service";
|
import { TokensService } from "./providers/tokens.service";
|
||||||
import { JwtStrategy } from "./strategies/jwt.strategy";
|
|
||||||
import { NotificationModule } from "../notifications/notifications.module";
|
import { NotificationModule } from "../notifications/notifications.module";
|
||||||
import { UsersModule } from "../users/users.module";
|
import { UsersModule } from "../users/users.module";
|
||||||
|
import { ConsoleJwtStrategy } from "./strategies/console-jwt.strategy";
|
||||||
|
import { LocalJwtStrategy } from "./strategies/local-jwt.strategy";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig()), NotificationModule],
|
imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig()), NotificationModule],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [AuthService, TokensService, JwtStrategy],
|
providers: [AuthService, TokensService, LocalJwtStrategy, ConsoleJwtStrategy],
|
||||||
exports: [AuthService],
|
exports: [AuthService],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { Reflector } from "@nestjs/core";
|
|||||||
import { AuthGuard } from "@nestjs/passport";
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
import { Observable } from "rxjs";
|
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 { SKIP_AUTH_KEY } from "../../../common/decorators/skip-auth.decorator";
|
||||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
@Injectable()
|
@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) {
|
constructor(private reflector: Reflector) {
|
||||||
super();
|
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";
|
import { RoleEnum } from "../../users/enums/role.enum";
|
||||||
|
|
||||||
export interface ITokenPayload {
|
export interface ILocalTokenPayload {
|
||||||
id: string;
|
id: string;
|
||||||
role: RoleEnum;
|
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 { PassportStrategy } from "@nestjs/passport";
|
||||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
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";
|
import { ITokenPayload } from "../interfaces/IToken-payload";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) {
|
export class LocalJwtStrategy extends PassportStrategy(Strategy, LOCAL_JWT_STRATEGY_NAME) {
|
||||||
constructor(configService: ConfigService) {
|
constructor(configService: ConfigService) {
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import { Cascade, Collection, Entity, EntityRepositoryType, OneToMany, Property, Unique } from "@mikro-orm/core";
|
import { Cascade, Collection, Entity, EntityRepositoryType, OneToMany, Property, Unique } from "@mikro-orm/core";
|
||||||
|
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { Announcement } from "../../announcements/entities/announcement.entity";
|
||||||
import { Company } from "../../companies/entities/company.entity";
|
import { Company } from "../../companies/entities/company.entity";
|
||||||
import { Industry } from "../../industries/entities/industry.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";
|
import { BusinessRepository } from "../repositories/business.repository";
|
||||||
|
|
||||||
@Entity({ repository: () => BusinessRepository })
|
@Entity({ repository: () => BusinessRepository })
|
||||||
@@ -23,5 +27,17 @@ export class Business extends BaseEntity {
|
|||||||
@OneToMany(() => Industry, (industry) => industry.business)
|
@OneToMany(() => Industry, (industry) => industry.business)
|
||||||
industries = new Collection<Industry>(this);
|
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;
|
[EntityRepositoryType]?: BusinessRepository;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { CompanyListQueryDto } from "./DTO/company-list-query.dto";
|
|||||||
import { CreateCompanyDto } from "./DTO/create-company.dto";
|
import { CreateCompanyDto } from "./DTO/create-company.dto";
|
||||||
import { UpdateCompanyDto } from "./DTO/update-company.dto";
|
import { UpdateCompanyDto } from "./DTO/update-company.dto";
|
||||||
import { CompaniesService } from "./services/companies.service";
|
import { CompaniesService } from "./services/companies.service";
|
||||||
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
import { BusinessDec } from "../../common/decorators/business.decorator";
|
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||||
import { ParamDto } from "../../common/DTO/param.dto";
|
import { ParamDto } from "../../common/DTO/param.dto";
|
||||||
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
||||||
@@ -15,36 +16,42 @@ export class CompaniesController {
|
|||||||
constructor(private readonly companiesService: CompaniesService) {}
|
constructor(private readonly companiesService: CompaniesService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "create company (admin)" })
|
@ApiOperation({ summary: "create company (admin)" })
|
||||||
create(@Body() createCompanyDto: CreateCompanyDto, @BusinessDec() business: Business) {
|
create(@Body() createCompanyDto: CreateCompanyDto, @BusinessDec() business: Business) {
|
||||||
return this.companiesService.create(createCompanyDto, business);
|
return this.companiesService.create(createCompanyDto, business);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(":id")
|
@Patch(":id")
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "update company (admin)" })
|
@ApiOperation({ summary: "update company (admin)" })
|
||||||
update(@Param() paramDto: ParamDto, @Body() updateCompanyDto: UpdateCompanyDto, @BusinessDec("id") businessId: string) {
|
update(@Param() paramDto: ParamDto, @Body() updateCompanyDto: UpdateCompanyDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.companiesService.updateCompanyById(paramDto.id, updateCompanyDto, businessId);
|
return this.companiesService.updateCompanyById(paramDto.id, updateCompanyDto, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(":id")
|
@Delete(":id")
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "delete company (admin)" })
|
@ApiOperation({ summary: "delete company (admin)" })
|
||||||
delete(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
delete(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.companiesService.deleteCompanyById(paramDto.id, businessId);
|
return this.companiesService.deleteCompanyById(paramDto.id, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("list")
|
@Get("list")
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "get companies list (admin)" })
|
@ApiOperation({ summary: "get companies list (admin)" })
|
||||||
getCompaniesList(@Query() query: CompanyListQueryDto, @BusinessDec("id") businessId: string) {
|
getCompaniesList(@Query() query: CompanyListQueryDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.companiesService.getCompaniesList(query, businessId);
|
return this.companiesService.getCompaniesList(query, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(":id")
|
@Get(":id")
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "get company by id" })
|
@ApiOperation({ summary: "get company by id" })
|
||||||
get(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
get(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.companiesService.getCompanyById(paramDto.id, businessId);
|
return this.companiesService.getCompanyById(paramDto.id, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(":id/toggle-status")
|
@Patch(":id/toggle-status")
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "toggle company status (admin)" })
|
@ApiOperation({ summary: "toggle company status (admin)" })
|
||||||
toggleStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
toggleStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.companiesService.toggleStatus(paramDto.id, businessId);
|
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 { IndustryListQueryDto } from "./DTO/industry-list-query.dto";
|
||||||
import { UpdateIndustryDto } from "./DTO/update-industry.dto";
|
import { UpdateIndustryDto } from "./DTO/update-industry.dto";
|
||||||
import { IndustriesService } from "./services/industries.service";
|
import { IndustriesService } from "./services/industries.service";
|
||||||
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
import { BusinessDec } from "../../common/decorators/business.decorator";
|
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||||
import { ParamDto } from "../../common/DTO/param.dto";
|
import { ParamDto } from "../../common/DTO/param.dto";
|
||||||
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
||||||
@@ -16,6 +17,7 @@ export class IndustriesController {
|
|||||||
constructor(private readonly industriesService: IndustriesService) {}
|
constructor(private readonly industriesService: IndustriesService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "create industry" })
|
@ApiOperation({ summary: "create industry" })
|
||||||
create(@Body() createIndustryDto: CreateIndustryDto, @BusinessDec() business: Business) {
|
create(@Body() createIndustryDto: CreateIndustryDto, @BusinessDec() business: Business) {
|
||||||
return this.industriesService.create(createIndustryDto, business);
|
return this.industriesService.create(createIndustryDto, business);
|
||||||
@@ -28,24 +30,28 @@ export class IndustriesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(":id")
|
@Patch(":id")
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "update industry" })
|
@ApiOperation({ summary: "update industry" })
|
||||||
updateIndustry(@Param() paramDto: ParamDto, @Body() updateIndustryDto: UpdateIndustryDto, @BusinessDec("id") businessId: string) {
|
updateIndustry(@Param() paramDto: ParamDto, @Body() updateIndustryDto: UpdateIndustryDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.industriesService.updateIndustry(paramDto.id, updateIndustryDto, businessId);
|
return this.industriesService.updateIndustry(paramDto.id, updateIndustryDto, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(":id")
|
@Delete(":id")
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "delete industry" })
|
@ApiOperation({ summary: "delete industry" })
|
||||||
deleteIndustry(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
deleteIndustry(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.industriesService.deleteIndustry(paramDto.id, businessId);
|
return this.industriesService.deleteIndustry(paramDto.id, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("list")
|
@Get("list")
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "get industries list for admin" })
|
@ApiOperation({ summary: "get industries list for admin" })
|
||||||
getIndustriesForAdmin(@Query() query: IndustryListQueryDto, @BusinessDec("id") businessId: string) {
|
getIndustriesForAdmin(@Query() query: IndustryListQueryDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.industriesService.getIndustriesListForAdmin(query, businessId);
|
return this.industriesService.getIndustriesListForAdmin(query, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(":id/toggle-status")
|
@Patch(":id/toggle-status")
|
||||||
|
@AuthGuards()
|
||||||
@ApiOperation({ summary: "toggle industry status" })
|
@ApiOperation({ summary: "toggle industry status" })
|
||||||
toggleStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
toggleStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.industriesService.toggleStatus(paramDto.id, businessId);
|
return this.industriesService.toggleStatus(paramDto.id, businessId);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Decimal } from "decimal.js";
|
|||||||
|
|
||||||
import { InvoiceItem } from "./invoice-item.entity";
|
import { InvoiceItem } from "./invoice-item.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { Business } from "../../businesses/entities/business.entity";
|
||||||
import { Company } from "../../companies/entities/company.entity";
|
import { Company } from "../../companies/entities/company.entity";
|
||||||
import { RecurringPeriodEnum } from "../enums/invoice-recurring-period.enum";
|
import { RecurringPeriodEnum } from "../enums/invoice-recurring-period.enum";
|
||||||
import { InvoiceStatus } from "../enums/invoice-status.enum";
|
import { InvoiceStatus } from "../enums/invoice-status.enum";
|
||||||
@@ -10,7 +11,7 @@ import { InvoicesRepository } from "../repositories/invoices.repository";
|
|||||||
|
|
||||||
@Entity({ repository: () => InvoicesRepository })
|
@Entity({ repository: () => InvoicesRepository })
|
||||||
export class Invoice extends BaseEntity {
|
export class Invoice extends BaseEntity {
|
||||||
@Property({ type: "int", autoincrement: true, persist: false })
|
@Property({ type: "int", autoincrement: true })
|
||||||
numericId!: number & Opt;
|
numericId!: number & Opt;
|
||||||
|
|
||||||
@ManyToOne(() => Company, { nullable: false, deleteRule: "cascade" })
|
@ManyToOne(() => Company, { nullable: false, deleteRule: "cascade" })
|
||||||
@@ -40,7 +41,7 @@ export class Invoice extends BaseEntity {
|
|||||||
@Property({ type: "boolean", default: false })
|
@Property({ type: "boolean", default: false })
|
||||||
isRecurring: boolean & Opt;
|
isRecurring: boolean & Opt;
|
||||||
|
|
||||||
@Enum({ items: () => RecurringPeriodEnum, nativeEnumName: "recurring_period", nullable: true })
|
@Enum({ items: () => RecurringPeriodEnum, nullable: true, type: "smallint" })
|
||||||
recurringPeriod?: RecurringPeriodEnum;
|
recurringPeriod?: RecurringPeriodEnum;
|
||||||
|
|
||||||
@Property({ type: "int", default: 0 })
|
@Property({ type: "int", default: 0 })
|
||||||
@@ -52,5 +53,8 @@ export class Invoice extends BaseEntity {
|
|||||||
@OneToMany(() => InvoiceItem, (item) => item.invoice, { cascade: [Cascade.PERSIST, Cascade.REMOVE] })
|
@OneToMany(() => InvoiceItem, (item) => item.invoice, { cascade: [Cascade.PERSIST, Cascade.REMOVE] })
|
||||||
items = new Collection<InvoiceItem>(this);
|
items = new Collection<InvoiceItem>(this);
|
||||||
|
|
||||||
|
@ManyToOne(() => Business, { nullable: false, deleteRule: "cascade" })
|
||||||
|
business!: Business;
|
||||||
|
|
||||||
[EntityRepositoryType]?: InvoicesRepository;
|
[EntityRepositoryType]?: InvoicesRepository;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export enum InvoiceStatus {
|
|||||||
PENDING = "PENDING",
|
PENDING = "PENDING",
|
||||||
WAIT_PAYMENT = "WAIT_PAYMENT",
|
WAIT_PAYMENT = "WAIT_PAYMENT",
|
||||||
PAID = "PAID",
|
PAID = "PAID",
|
||||||
|
ARCHIVED = "ARCHIVED",
|
||||||
OVERDUE = "OVERDUE",
|
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 { ApiOperation } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { CreateInvoiceDto } from "./DTO/create-invoice.dto";
|
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 { UpdateInvoiceDto } from "./DTO/update-invoice.dto";
|
||||||
import { InvoicesService } from "./providers/invoices.service";
|
import { InvoicesService } from "./providers/invoices.service";
|
||||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
|
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||||
import { UserDec } from "../../common/decorators/user.decorator";
|
import { UserDec } from "../../common/decorators/user.decorator";
|
||||||
import { ParamDto } from "../../common/DTO/param.dto";
|
import { ParamDto } from "../../common/DTO/param.dto";
|
||||||
|
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
||||||
import { VerifyOtpWithUserId } from "../auth/DTO/verify-otp.dto";
|
import { VerifyOtpWithUserId } from "../auth/DTO/verify-otp.dto";
|
||||||
|
import { Business } from "../businesses/entities/business.entity";
|
||||||
import { RoleEnum } from "../users/enums/role.enum";
|
import { RoleEnum } from "../users/enums/role.enum";
|
||||||
|
|
||||||
@Controller("invoices")
|
@Controller("invoices")
|
||||||
|
@UseInterceptors(BusinessInterceptor)
|
||||||
@AuthGuards()
|
@AuthGuards()
|
||||||
export class InvoicesController {
|
export class InvoicesController {
|
||||||
constructor(private readonly invoiceService: InvoicesService) {}
|
constructor(private readonly invoiceService: InvoicesService) {}
|
||||||
|
|
||||||
@ApiOperation({ summary: "create an invoice (admin)" })
|
@ApiOperation({ summary: "create an invoice (admin)" })
|
||||||
@Post()
|
@Post()
|
||||||
createInvoice(@Body() createDto: CreateInvoiceDto) {
|
createInvoice(@Body() createDto: CreateInvoiceDto, @BusinessDec() business: Business) {
|
||||||
return this.invoiceService.createInvoiceAdmin(createDto);
|
return this.invoiceService.createInvoiceAdmin(createDto, business);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "update an invoice (admin)" })
|
@ApiOperation({ summary: "update an invoice (admin)" })
|
||||||
@Patch(":id")
|
@Patch(":id")
|
||||||
updateInvoice(@Param() paramDto: ParamDto, @Body() updateDto: UpdateInvoiceDto) {
|
updateInvoice(@Param() paramDto: ParamDto, @Body() updateDto: UpdateInvoiceDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.invoiceService.updateInvoiceAdmin(paramDto.id, updateDto);
|
return this.invoiceService.updateInvoiceAdmin(paramDto.id, updateDto, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "get all invoices (admin)" })
|
@ApiOperation({ summary: "get all invoices (admin)" })
|
||||||
@Get()
|
@Get()
|
||||||
getInvoices(@Query() queryDto: InvoicesSearchQueryDto) {
|
getInvoices(@Query() queryDto: InvoicesSearchQueryDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.invoiceService.getInvoices(queryDto);
|
return this.invoiceService.getInvoices(queryDto, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "get all user invoices" })
|
@ApiOperation({ summary: "get all user invoices" })
|
||||||
@Get("user")
|
@Get("user")
|
||||||
getUserInvoices(@Query() queryDto: UserInvoicesSearchQueryDto, @UserDec("id") userId: string) {
|
getUserInvoices(@Query() queryDto: UserInvoicesSearchQueryDto, @UserDec("id") userId: string, @BusinessDec("id") businessId: string) {
|
||||||
return this.invoiceService.getUserInvoices(queryDto, userId);
|
return this.invoiceService.getUserInvoices(queryDto, userId, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "get single invoice by Id " })
|
@ApiOperation({ summary: "get single invoice by Id " })
|
||||||
@Get(":id")
|
@Get(":id")
|
||||||
getInvoiceById(@Param() paramDto: ParamDto, @UserDec("role") role: RoleEnum, @UserDec("id") userId: string) {
|
getInvoiceById(
|
||||||
return this.invoiceService.getInvoiceById(paramDto.id, role, userId);
|
@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" })
|
@ApiOperation({ summary: "approve request invoice by user" })
|
||||||
@Post(":id/approve/request")
|
@Post(":id/approve/request")
|
||||||
approveRequestInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
approveRequestInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @BusinessDec("id") businessId: string) {
|
||||||
return this.invoiceService.approveInvoiceRequest(paramDto.id, userId);
|
return this.invoiceService.approveInvoiceRequest(paramDto.id, userId, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "approve invoice by user" })
|
@ApiOperation({ summary: "approve invoice by user" })
|
||||||
@Patch(":id/approve")
|
@Patch(":id/approve")
|
||||||
approveInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @Body() verifyOtpDto: VerifyOtpWithUserId) {
|
approveInvoiceByUser(
|
||||||
return this.invoiceService.approveInvoiceByUser(paramDto.id, userId, verifyOtpDto);
|
@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" })
|
// @ApiOperation({ summary: "pay invoice by user" })
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Invoice } from "./entities/invoice.entity";
|
|||||||
import { InvoicesController } from "./invoices.controller";
|
import { InvoicesController } from "./invoices.controller";
|
||||||
import { InvoicesService } from "./providers/invoices.service";
|
import { InvoicesService } from "./providers/invoices.service";
|
||||||
import { InvoiceProcessor } from "./queue/invoice.processor";
|
import { InvoiceProcessor } from "./queue/invoice.processor";
|
||||||
|
import { BusinessesModule } from "../businesses/businesses.module";
|
||||||
import { CompaniesModule } from "../companies/companies.module";
|
import { CompaniesModule } from "../companies/companies.module";
|
||||||
import { NotificationModule } from "../notifications/notifications.module";
|
import { NotificationModule } from "../notifications/notifications.module";
|
||||||
import { UsersModule } from "../users/users.module";
|
import { UsersModule } from "../users/users.module";
|
||||||
@@ -21,6 +22,7 @@ import { UtilsModule } from "../utils/utils.module";
|
|||||||
UtilsModule,
|
UtilsModule,
|
||||||
NotificationModule,
|
NotificationModule,
|
||||||
CompaniesModule,
|
CompaniesModule,
|
||||||
|
BusinessesModule,
|
||||||
],
|
],
|
||||||
providers: [InvoicesService, InvoiceProcessor],
|
providers: [InvoicesService, InvoiceProcessor],
|
||||||
controllers: [InvoicesController],
|
controllers: [InvoicesController],
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import Decimal from "decimal.js";
|
|||||||
|
|
||||||
import { AuthMessage, InvoiceMessage } from "../../../common/enums/message.enum";
|
import { AuthMessage, InvoiceMessage } from "../../../common/enums/message.enum";
|
||||||
import { VerifyOtpWithUserId } from "../../auth/DTO/verify-otp.dto";
|
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 { CompaniesService } from "../../companies/services/companies.service";
|
||||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||||
import { User } from "../../users/entities/user.entity";
|
|
||||||
import { RoleEnum } from "../../users/enums/role.enum";
|
import { RoleEnum } from "../../users/enums/role.enum";
|
||||||
import { UsersService } from "../../users/services/users.service";
|
import { UsersService } from "../../users/services/users.service";
|
||||||
import { OTPService } from "../../utils/providers/otp.service";
|
import { OTPService } from "../../utils/providers/otp.service";
|
||||||
@@ -41,31 +42,7 @@ export class InvoicesService {
|
|||||||
|
|
||||||
///********************************** */
|
///********************************** */
|
||||||
|
|
||||||
private validateInvoiceItems(items: InvoiceItemDto[]): void {
|
async createInvoiceAdmin(createDto: CreateInvoiceDto, business: Business) {
|
||||||
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) {
|
|
||||||
const em = this.em.fork();
|
const em = this.em.fork();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -85,7 +62,7 @@ export class InvoicesService {
|
|||||||
return invoiceItem;
|
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);
|
const tax = totalPrice.mul(0.1);
|
||||||
|
|
||||||
if (totalPrice.lessThanOrEqualTo(0)) throw new BadRequestException(InvoiceMessage.TOTAL_PRICE_MUST_BE_POSITIVE);
|
if (totalPrice.lessThanOrEqualTo(0)) throw new BadRequestException(InvoiceMessage.TOTAL_PRICE_MUST_BE_POSITIVE);
|
||||||
@@ -106,6 +83,7 @@ export class InvoicesService {
|
|||||||
recurringPeriod: createDto.recurringPeriod,
|
recurringPeriod: createDto.recurringPeriod,
|
||||||
maxRecurringCycles: createDto.maxRecurringCycles,
|
maxRecurringCycles: createDto.maxRecurringCycles,
|
||||||
currentRecurringCycle: 0,
|
currentRecurringCycle: 0,
|
||||||
|
business,
|
||||||
});
|
});
|
||||||
|
|
||||||
await em.persistAndFlush(invoice);
|
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();
|
const em = this.em.fork();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await em.begin();
|
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);
|
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
||||||
|
|
||||||
@@ -170,7 +148,7 @@ export class InvoicesService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Calculate total price and tax
|
// 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);
|
const tax = totalPrice.mul(0.1);
|
||||||
|
|
||||||
invoice.totalPrice = totalPrice.add(tax);
|
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();
|
const em = this.em.fork();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -207,7 +185,7 @@ export class InvoicesService {
|
|||||||
|
|
||||||
const user = await this.usersService.findOneByIdWithEntityManager(userId, em);
|
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");
|
const existCode = await this.otpService.checkExistOtp(user.phone, "INVOICE_VERIFY");
|
||||||
if (existCode) {
|
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();
|
const em = this.em.fork();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -256,7 +234,7 @@ export class InvoicesService {
|
|||||||
const isValid = await this.otpService.verifyOtp(user.phone, code, "INVOICE_VERIFY");
|
const isValid = await this.otpService.verifyOtp(user.phone, code, "INVOICE_VERIFY");
|
||||||
if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP);
|
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;
|
invoice.status = InvoiceStatus.WAIT_PAYMENT;
|
||||||
await em.persistAndFlush(invoice);
|
await em.persistAndFlush(invoice);
|
||||||
@@ -286,8 +264,12 @@ export class InvoicesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//********************************** */
|
//********************************** */
|
||||||
private async validateInvoiceForApproval(invoiceId: string, userId: string, em: EntityManager): Promise<Invoice> {
|
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 } } }, { populate: ["items"] });
|
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);
|
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 (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID);
|
||||||
|
|
||||||
if (dayjs().isAfter(dayjs(invoice.dueDate).add(INVOICE.MAX_DAYS_AFTER_OVERDUE, "day"))) {
|
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);
|
await em.persistAndFlush(invoice);
|
||||||
throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE);
|
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 companiesThatHaveInvoices = await this.em
|
||||||
const [invoices, count] = await this.invoiceRepository.getInvoicesForAdmin(queryDto);
|
.createQueryBuilder(Company, "company")
|
||||||
|
.select(["company.id", "company.name"])
|
||||||
// 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"])
|
|
||||||
.distinct()
|
.distinct()
|
||||||
.join("user.invoices", "invoice")
|
.join("company.invoices", "invoice")
|
||||||
.getResult();
|
.getResult();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users: usersThatHaveInvoices,
|
companies: companiesThatHaveInvoices,
|
||||||
invoices,
|
invoices,
|
||||||
count,
|
count,
|
||||||
paginate: true,
|
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;
|
let invoice: Invoice | null;
|
||||||
|
|
||||||
if (role === RoleEnum.ADMIN) {
|
if (role === RoleEnum.USER) {
|
||||||
invoice = await this.invoiceRepository.findOne(invoiceId, { populate: ["items", "company"] });
|
invoice = await this.invoiceRepository.findOne(
|
||||||
|
{ id: invoiceId, company: { user: { id: userId } }, business: { id: businessId } },
|
||||||
|
{ populate: ["items", "company"] },
|
||||||
|
);
|
||||||
} else {
|
} 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);
|
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 { limit, skip } = PaginationUtils(queryDto);
|
||||||
|
|
||||||
const qb = this.em
|
const qb = this.em
|
||||||
.createQueryBuilder(Invoice, "invoice")
|
.createQueryBuilder(Invoice, "invoice")
|
||||||
.leftJoinAndSelect("invoice.items", "items")
|
.leftJoinAndSelect("invoice.items", "items")
|
||||||
.where({ company: { user: { id: userId } } })
|
.where({ company: { user: { id: userId } }, business: { id: businessId } })
|
||||||
.orderBy({ createdAt: "DESC" });
|
.orderBy({ createdAt: "DESC" });
|
||||||
|
|
||||||
if (queryDto.status) {
|
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) {
|
async process(job: Job, token?: string) {
|
||||||
this.logger.log(job);
|
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
case INVOICE.REMINDER_JOB_NAME:
|
case INVOICE.REMINDER_JOB_NAME:
|
||||||
return this.sendBillInvoiceReminder(job, token);
|
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}`);
|
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,
|
recurringPeriod: invoice.recurringPeriod,
|
||||||
maxRecurringCycles: invoice.maxRecurringCycles,
|
maxRecurringCycles: invoice.maxRecurringCycles,
|
||||||
currentRecurringCycle: invoice.currentRecurringCycle + 1,
|
currentRecurringCycle: invoice.currentRecurringCycle + 1,
|
||||||
|
business: invoice.business,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the original invoice's current recurring cycle
|
// Update the original invoice's current recurring cycle
|
||||||
@@ -149,10 +149,10 @@ export class InvoiceProcessor extends WorkerProcessor {
|
|||||||
//********************************** */
|
//********************************** */
|
||||||
|
|
||||||
private async cancelOverdueInvoice(invoice: Invoice, em: EntityManager) {
|
private async cancelOverdueInvoice(invoice: Invoice, em: EntityManager) {
|
||||||
invoice.status = InvoiceStatus.CANCELLED;
|
invoice.status = InvoiceStatus.ARCHIVED;
|
||||||
await em.persistAndFlush(invoice);
|
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> {
|
export class InvoicesRepository extends EntityRepository<Invoice> {
|
||||||
//
|
//
|
||||||
async getInvoicesForAdmin(queryDto: InvoicesSearchQueryDto) {
|
async getInvoicesForAdmin(queryDto: InvoicesSearchQueryDto, businessId: string) {
|
||||||
const { limit, skip } = PaginationUtils(queryDto);
|
const { limit, skip } = PaginationUtils(queryDto);
|
||||||
|
|
||||||
const where: FilterQuery<Invoice> = {};
|
const where: FilterQuery<Invoice> = { business: { id: businessId } };
|
||||||
|
|
||||||
if (queryDto.q) {
|
if (queryDto.q) {
|
||||||
where.$or = [
|
where.$or = [
|
||||||
@@ -25,7 +25,7 @@ export class InvoicesRepository extends EntityRepository<Invoice> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (queryDto.companyId) {
|
if (queryDto.companyId) {
|
||||||
where.company = queryDto.companyId;
|
where.company = { id: queryDto.companyId };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryDto.since || queryDto.to) {
|
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 { 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";
|
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 })
|
@ApiProperty({ description: "Description of the ticket category", example: "درخواستهای فنی", minLength: 3, maxLength: 500 })
|
||||||
description: string;
|
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 })
|
@IsNotEmpty({ message: TicketMessageEnum.IS_ACTIVE_REQUIRED })
|
||||||
@IsBoolean({ message: TicketMessageEnum.IS_ACTIVE_SHOULD_BE_BOOLEAN })
|
@IsBoolean({ message: TicketMessageEnum.IS_ACTIVE_SHOULD_BE_BOOLEAN })
|
||||||
@ApiProperty({ description: "Is active", example: true })
|
@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";
|
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 { Ticket } from "./ticket.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.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 {
|
export class TicketCategory extends BaseEntity {
|
||||||
@Property({ type: "varchar", length: 150, unique: true })
|
@Property({ type: "varchar", length: 150, unique: true })
|
||||||
title!: string;
|
title!: string;
|
||||||
@@ -14,6 +16,14 @@ export class TicketCategory extends BaseEntity {
|
|||||||
@Property({ type: "boolean", default: true })
|
@Property({ type: "boolean", default: true })
|
||||||
isActive: boolean = 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)
|
@OneToMany(() => Ticket, (ticket) => ticket.category)
|
||||||
tickets = new Collection<Ticket>(this);
|
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 { TicketMessage } from "./ticket-message.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.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 {
|
export class TicketMessageAttachment extends BaseEntity {
|
||||||
@Property({ type: "varchar", length: 150, nullable: true })
|
@Property({ type: "varchar", length: 150, nullable: true })
|
||||||
attachmentUrl?: string;
|
attachmentUrl?: string;
|
||||||
|
|
||||||
@ManyToOne({ entity: () => TicketMessage, deleteRule: "cascade", nullable: false })
|
@ManyToOne({ entity: () => TicketMessage, deleteRule: "cascade", nullable: false })
|
||||||
ticketMessage!: TicketMessage;
|
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 { TicketMessageAttachment } from "./ticket-message-attachment";
|
||||||
import { Ticket } from "./ticket.entity";
|
import { Ticket } from "./ticket.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
import { TicketMessagesRepository } from "../repositories/tickets-message.repository";
|
||||||
|
|
||||||
@Entity()
|
@Entity({ repository: () => TicketMessagesRepository })
|
||||||
export class TicketMessage extends BaseEntity {
|
export class TicketMessage extends BaseEntity {
|
||||||
@Property({ type: "text" })
|
@Property({ type: "text" })
|
||||||
content!: string;
|
content!: string;
|
||||||
@@ -18,4 +19,6 @@ export class TicketMessage extends BaseEntity {
|
|||||||
|
|
||||||
@ManyToOne(() => User, { deleteRule: "cascade", nullable: false })
|
@ManyToOne(() => User, { deleteRule: "cascade", nullable: false })
|
||||||
author!: User;
|
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 { TicketCategory } from "./ticket-category.entity";
|
||||||
import { TicketMessage } from "./ticket-message.entity";
|
import { TicketMessage } from "./ticket-message.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { Business } from "../../businesses/entities/business.entity";
|
||||||
import { Company } from "../../companies/entities/company.entity";
|
import { Company } from "../../companies/entities/company.entity";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
import { TicketPriority } from "../enums/ticket-priority.enum";
|
import { TicketPriority } from "../enums/ticket-priority.enum";
|
||||||
import { TicketStatus } from "../enums/ticket-status.enum";
|
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||||
|
import { TicketsRepository } from "../repositories/tickets.repository";
|
||||||
|
|
||||||
@Entity()
|
@Entity({ repository: () => TicketsRepository })
|
||||||
export class Ticket extends BaseEntity {
|
export class Ticket extends BaseEntity {
|
||||||
@Property({ type: "int", autoincrement: true, persist: false })
|
@Property({ type: "int", autoincrement: true, persist: false })
|
||||||
numericId!: number & Opt;
|
numericId!: number & Opt;
|
||||||
@@ -34,6 +36,11 @@ export class Ticket extends BaseEntity {
|
|||||||
@ManyToOne(() => TicketCategory, { deleteRule: "set null", nullable: true })
|
@ManyToOne(() => TicketCategory, { deleteRule: "set null", nullable: true })
|
||||||
category?: TicketCategory;
|
category?: TicketCategory;
|
||||||
|
|
||||||
|
@ManyToOne(() => Business, { deleteRule: "cascade", nullable: false })
|
||||||
|
business!: Business;
|
||||||
|
|
||||||
@OneToMany(() => TicketMessage, (message) => message.ticket, { cascade: [Cascade.ALL] })
|
@OneToMany(() => TicketMessage, (message) => message.ticket, { cascade: [Cascade.ALL] })
|
||||||
messages = new Collection<TicketMessage>(this);
|
messages = new Collection<TicketMessage>(this);
|
||||||
|
|
||||||
|
[EntityRepositoryType]?: TicketsRepository;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { BadRequestException, HttpException, Injectable, InternalServerErrorExce
|
|||||||
|
|
||||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||||
import { CommonMessage, CompanyMessage, TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum";
|
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 { Company } from "../../companies/entities/company.entity";
|
||||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
@@ -38,26 +39,27 @@ export class TicketsService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
//******************************** */
|
//******************************** */
|
||||||
async getTicketCategories() {
|
async getTicketCategories(businessId: string) {
|
||||||
const ticketCategories = await this.ticketsCategoryRepository.find({});
|
const ticketCategories = await this.ticketsCategoryRepository.find({ business: { id: businessId } });
|
||||||
return {
|
return {
|
||||||
ticketCategories,
|
ticketCategories,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
//******************************** */
|
//******************************** */
|
||||||
|
|
||||||
async getCategoriesList(queryDto: SearchTicketCategoryDto) {
|
async getCategoriesList(queryDto: SearchTicketCategoryDto, businessId: string) {
|
||||||
const [categories, count] = await this.ticketsCategoryRepository.findCategoriesList(queryDto);
|
const [categories, count] = await this.ticketsCategoryRepository.findCategoriesList(queryDto, businessId);
|
||||||
|
|
||||||
return { categories, count, paginate: true };
|
return { categories, count, paginate: true };
|
||||||
}
|
}
|
||||||
//******************************** */
|
//******************************** */
|
||||||
async createTicketCategory(createDto: CreateTicketCategoryDto) {
|
async createTicketCategory(createDto: CreateTicketCategoryDto, business: Business) {
|
||||||
const existCategory = await this.ticketsCategoryRepository.findCategoryByTitle(createDto.title);
|
const existCategory = await this.ticketsCategoryRepository.findCategoryByTitle(createDto.title, business.id);
|
||||||
|
|
||||||
if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST);
|
if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST);
|
||||||
const ticketCategory = this.ticketsCategoryRepository.create({
|
const ticketCategory = this.ticketsCategoryRepository.create({
|
||||||
...createDto,
|
...createDto,
|
||||||
|
business,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.em.persistAndFlush(ticketCategory);
|
await this.em.persistAndFlush(ticketCategory);
|
||||||
@@ -69,8 +71,8 @@ export class TicketsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//******************************** */
|
//******************************** */
|
||||||
async updateCategory(paramDto: ParamDto, updateCategoryDto: UpdateTicketCategoryDto) {
|
async updateCategory(paramDto: ParamDto, updateCategoryDto: UpdateTicketCategoryDto, businessId: string) {
|
||||||
const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id);
|
const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id, businessId);
|
||||||
if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
||||||
|
|
||||||
if (updateCategoryDto.title) {
|
if (updateCategoryDto.title) {
|
||||||
@@ -88,9 +90,25 @@ export class TicketsService {
|
|||||||
message: CommonMessage.UPDATE_SUCCESS,
|
message: CommonMessage.UPDATE_SUCCESS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//******************************** */
|
//******************************** */
|
||||||
async toggleCategoryStatus(paramDto: ParamDto) {
|
async deleteCategory(paramDto: ParamDto, businessId: string) {
|
||||||
const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id);
|
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);
|
if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
||||||
//
|
//
|
||||||
category.isActive = !category.isActive;
|
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();
|
const em = this.em.fork();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -142,6 +160,7 @@ export class TicketsService {
|
|||||||
category: ticketCategory,
|
category: ticketCategory,
|
||||||
messages: [ticketMessage],
|
messages: [ticketMessage],
|
||||||
company,
|
company,
|
||||||
|
business,
|
||||||
});
|
});
|
||||||
|
|
||||||
await em.persistAndFlush(ticket);
|
await em.persistAndFlush(ticket);
|
||||||
@@ -163,27 +182,27 @@ export class TicketsService {
|
|||||||
|
|
||||||
//******************************** */
|
//******************************** */
|
||||||
|
|
||||||
async getTickets(queryDto: SearchTicketQueryDto, userId: string) {
|
async getTickets(queryDto: SearchTicketQueryDto, userId: string, businessId: string) {
|
||||||
const [tickets, count] = await this.ticketsRepository.findTicketsList(queryDto, userId);
|
const [tickets, count] = await this.ticketsRepository.findTicketsList(queryDto, userId, businessId);
|
||||||
|
|
||||||
return { tickets, count, paginate: true };
|
return { tickets, count, paginate: true };
|
||||||
}
|
}
|
||||||
//******************************** */
|
//******************************** */
|
||||||
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string) {
|
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string, businessId: string) {
|
||||||
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId);
|
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId, businessId);
|
||||||
|
|
||||||
return { tickets, count, paginate: true };
|
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();
|
const em = this.em.fork();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await em.begin();
|
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 (ticket.status === TicketStatus.CLOSED) throw new BadRequestException(TicketMessageEnum.TICKET_CLOSED);
|
||||||
|
|
||||||
if (role === RoleEnum.ADMIN && ticket.assignedTo?.id !== userId)
|
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;
|
let ticket = null;
|
||||||
//
|
//
|
||||||
if (role === RoleEnum.ADMIN) {
|
if (role === RoleEnum.ADMIN) {
|
||||||
ticket = await this.ticketsRepository.findTicketById(ticketId);
|
ticket = await this.ticketsRepository.findTicketById(ticketId, businessId);
|
||||||
} else {
|
} 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);
|
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;
|
let ticket = null;
|
||||||
|
|
||||||
if (role === RoleEnum.ADMIN) {
|
if (role === RoleEnum.ADMIN) {
|
||||||
ticket = await this.ticketsRepository.findTicketById(ticketId);
|
ticket = await this.ticketsRepository.findTicketById(ticketId, businessId);
|
||||||
} else {
|
} 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);
|
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||||
@@ -262,8 +281,8 @@ export class TicketsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
//******************************** * /
|
//******************************** * /
|
||||||
async openTicketByAdmin(ticketId: string) {
|
async openTicketByAdmin(ticketId: string, businessId: string) {
|
||||||
const ticket = await this.ticketsRepository.findTicketById(ticketId);
|
const ticket = await this.ticketsRepository.findTicketById(ticketId, businessId);
|
||||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||||
|
|
||||||
ticket.status = TicketStatus.PENDING;
|
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();
|
const em = this.em.fork();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await em.begin();
|
await em.begin();
|
||||||
console.log(userId, "admin id");
|
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);
|
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||||
|
|
||||||
const admin = await this.usersService.findOneByIdWithEntityManager(referDto.adminId, em);
|
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
|
const ticket = isAdmin
|
||||||
? 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 } }, { 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);
|
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||||
return ticket;
|
return ticket;
|
||||||
|
|||||||
@@ -6,41 +6,18 @@ import { TicketCategory } from "../entities/ticket-category.entity";
|
|||||||
|
|
||||||
export class TicketCategoryRepository extends EntityRepository<TicketCategory> {
|
export class TicketCategoryRepository extends EntityRepository<TicketCategory> {
|
||||||
//
|
//
|
||||||
async findCategoryByTitle(title: string): Promise<TicketCategory | null> {
|
async findCategoryByTitle(title: string, businessId: string): Promise<TicketCategory | null> {
|
||||||
return this.findOne({ title });
|
return this.findOne({ title, business: { id: businessId }, deletedAt: null }, { populate: ["tickets"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
async findCategoryById(id: string): Promise<TicketCategory | null> {
|
async findCategoryById(id: string, businessId: string): Promise<TicketCategory | null> {
|
||||||
return this.findOne({ id });
|
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 { limit, skip } = PaginationUtils(queryDto);
|
||||||
|
|
||||||
const qb = this.createQueryBuilder("category")
|
const qb = this.createQueryBuilder("category").where({ business: { id: businessId }, deletedAt: null });
|
||||||
.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"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (queryDto.isActive !== undefined) {
|
if (queryDto.isActive !== undefined) {
|
||||||
qb.andWhere({ isActive: queryDto.isActive });
|
qb.andWhere({ isActive: queryDto.isActive });
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import { Ticket } from "../entities/ticket.entity";
|
|||||||
import { TicketStatus } from "../enums/ticket-status.enum";
|
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||||
|
|
||||||
export class TicketsRepository extends EntityRepository<Ticket> {
|
export class TicketsRepository extends EntityRepository<Ticket> {
|
||||||
async findTicketById(id: string, userId?: string) {
|
async findTicketById(id: string, businessId: string, userId?: string) {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
...(userId && { user: { id: userId } }),
|
...(userId && { user: { id: userId } }),
|
||||||
|
business: { id: businessId },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
populate: ["user", "category", "assignedTo", "company"],
|
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 { limit, skip } = PaginationUtils(queryDto);
|
||||||
|
|
||||||
const where: FilterQuery<Ticket> = { user: { id: userId } };
|
const where: FilterQuery<Ticket> = { user: { id: userId }, business: { id: businessId } };
|
||||||
if (queryDto.status) {
|
if (queryDto.status) {
|
||||||
where.status = queryDto.status;
|
where.status = queryDto.status;
|
||||||
} else {
|
} 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 { limit, skip } = PaginationUtils(queryDto);
|
||||||
|
|
||||||
const where: FilterQuery<Ticket> = {};
|
const where: FilterQuery<Ticket> = { business: { id: businessId } };
|
||||||
|
|
||||||
where.assignedTo = { id: adminId };
|
where.assignedTo = { id: adminId };
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from "@nestjs/common";
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common";
|
||||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { CreateTicketCategoryDto } from "./DTO/create-ticket-category.dto";
|
import { CreateTicketCategoryDto } from "./DTO/create-ticket-category.dto";
|
||||||
import { CreateTicketMessageDto } from "./DTO/create-ticket-message.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 { TicketsService } from "./providers/tickets.service";
|
||||||
import { AdminRoute } from "../../common/decorators/admin.decorator";
|
import { AdminRoute } from "../../common/decorators/admin.decorator";
|
||||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
|
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||||
import { UserDec } from "../../common/decorators/user.decorator";
|
import { UserDec } from "../../common/decorators/user.decorator";
|
||||||
import { ParamDto } from "../../common/DTO/param.dto";
|
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";
|
import { RoleEnum } from "../users/enums/role.enum";
|
||||||
|
|
||||||
@Controller("tickets")
|
@Controller("tickets")
|
||||||
@ApiTags("Tickets")
|
@UseInterceptors(BusinessInterceptor)
|
||||||
@AuthGuards()
|
@AuthGuards()
|
||||||
export class TicketsController {
|
export class TicketsController {
|
||||||
constructor(private readonly ticketsService: TicketsService) {}
|
constructor(private readonly ticketsService: TicketsService) {}
|
||||||
|
|
||||||
//----------------- ticket categories -------------------
|
//----------------- ticket categories -------------------
|
||||||
|
|
||||||
@ApiOperation({ summary: "Create ticket category => admin route" })
|
@ApiOperation({ summary: "Create ticket category (admin)" })
|
||||||
@Post("category")
|
@Post("category")
|
||||||
createTicketCategory(@Body() createDto: CreateTicketCategoryDto) {
|
createTicketCategory(@Body() createDto: CreateTicketCategoryDto, @BusinessDec() business: Business) {
|
||||||
return this.ticketsService.createTicketCategory(createDto);
|
return this.ticketsService.createTicketCategory(createDto, business);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "Update ticket category => admin route" })
|
@ApiOperation({ summary: "Update ticket category (admin)" })
|
||||||
@Patch("category/:id")
|
@Patch("category/:id")
|
||||||
updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateTicketCategoryDto) {
|
updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateTicketCategoryDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.ticketsService.updateCategory(paramDto, updateCategoryDto);
|
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 " })
|
@ApiOperation({ summary: "Get ticket categories " })
|
||||||
@Get("categories")
|
@Get("categories")
|
||||||
//
|
getTicketCategories(@BusinessDec("id") businessId: string) {
|
||||||
getTicketCategories() {
|
return this.ticketsService.getTicketCategories(businessId);
|
||||||
return this.ticketsService.getTicketCategories();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "Get ticket categories list => admin route" })
|
@ApiOperation({ summary: "Get ticket categories list (admin)" })
|
||||||
@AdminRoute()
|
@AdminRoute()
|
||||||
@Get("categories-list")
|
@Get("categories-list")
|
||||||
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) {
|
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.ticketsService.getCategoriesList(queryDto);
|
return this.ticketsService.getCategoriesList(queryDto, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "toggle status of categories => admin route" })
|
@ApiOperation({ summary: "toggle status of categories (admin)" })
|
||||||
@AdminRoute()
|
@AdminRoute()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post("category/toggle-status/:id")
|
@Post("category/toggle-status/:id")
|
||||||
toggleCategoryStatus(@Param() paramDto: ParamDto) {
|
toggleCategoryStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.ticketsService.toggleCategoryStatus(paramDto);
|
return this.ticketsService.toggleCategoryStatus(paramDto, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
//----------------- ticket and ticket messages -------------------
|
//----------------- ticket and ticket messages -------------------
|
||||||
|
|
||||||
@ApiOperation({ summary: "create ticket ==> user route" })
|
@ApiOperation({ summary: "create ticket (user)" })
|
||||||
@Post()
|
@Post()
|
||||||
createTicket(@Body() createDto: CreateTicketDto, @UserDec("id") userId: string) {
|
createTicket(@Body() createDto: CreateTicketDto, @UserDec("id") userId: string, @BusinessDec() business: Business) {
|
||||||
return this.ticketsService.createTicket(createDto, userId);
|
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()
|
@Get()
|
||||||
getTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") userId: string) {
|
getTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") userId: string, @BusinessDec("id") businessId: string) {
|
||||||
return this.ticketsService.getTickets(queryDto, userId);
|
return this.ticketsService.getTickets(queryDto, userId, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "Get all tickets ==> admin route" })
|
@ApiOperation({ summary: "Get all tickets (admin)" })
|
||||||
@AdminRoute()
|
@AdminRoute()
|
||||||
@Get("admin")
|
@Get("admin")
|
||||||
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string) {
|
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string, @BusinessDec("id") businessId: string) {
|
||||||
return this.ticketsService.getTicketForAdmin(queryDto, adminId);
|
return this.ticketsService.getTicketForAdmin(queryDto, adminId, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "create ticket messages" })
|
@ApiOperation({ summary: "create ticket messages" })
|
||||||
@@ -85,34 +93,50 @@ export class TicketsController {
|
|||||||
@Body() createDto: CreateTicketMessageDto,
|
@Body() createDto: CreateTicketMessageDto,
|
||||||
@UserDec("id") userId: string,
|
@UserDec("id") userId: string,
|
||||||
@UserDec("role") role: RoleEnum,
|
@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" })
|
@ApiOperation({ summary: "Get all ticket messages of user" })
|
||||||
@Get(":id/messages")
|
@Get(":id/messages")
|
||||||
getTicketMessages(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @UserDec("role") role: RoleEnum) {
|
getTicketMessages(
|
||||||
return this.ticketsService.getTicketMessages(paramDto.id, userId, role);
|
@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" })
|
@ApiOperation({ summary: "close ticket by user or admin" })
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post(":id/close")
|
@Post(":id/close")
|
||||||
closedTicketByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @UserDec("role") role: RoleEnum) {
|
closedTicketByUser(
|
||||||
return this.ticketsService.closeTicketByUser(paramDto.id, userId, role);
|
@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" })
|
@ApiOperation({ summary: "open ticket by admin" })
|
||||||
@AdminRoute()
|
@AdminRoute()
|
||||||
@Post(":id/open")
|
@Post(":id/open")
|
||||||
openTicketByAdmin(@Param() paramDto: ParamDto) {
|
openTicketByAdmin(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
|
||||||
return this.ticketsService.openTicketByAdmin(paramDto.id);
|
return this.ticketsService.openTicketByAdmin(paramDto.id, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "referer ticket ==> admin route" })
|
@ApiOperation({ summary: "referer ticket ==> admin route" })
|
||||||
@AdminRoute()
|
@AdminRoute()
|
||||||
@Post(":id/refer")
|
@Post(":id/refer")
|
||||||
referTicket(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @Body() referDto: ReferTicketDto) {
|
referTicket(
|
||||||
return this.ticketsService.referTicket(paramDto.id, userId, referDto);
|
@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 { TicketMessage } from "./entities/ticket-message.entity";
|
||||||
import { Ticket } from "./entities/ticket.entity";
|
import { Ticket } from "./entities/ticket.entity";
|
||||||
import { TicketsService } from "./providers/tickets.service";
|
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 { TicketsController } from "./tickets.controller";
|
||||||
import { UsersModule } from "../users/users.module";
|
import { UsersModule } from "../users/users.module";
|
||||||
import { TicketMessageAttachment } from "./entities/ticket-message-attachment";
|
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 { CompaniesModule } from "../companies/companies.module";
|
||||||
import { NotificationModule } from "../notifications/notifications.module";
|
import { NotificationModule } from "../notifications/notifications.module";
|
||||||
|
|
||||||
@@ -21,8 +18,9 @@ import { NotificationModule } from "../notifications/notifications.module";
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
NotificationModule,
|
NotificationModule,
|
||||||
CompaniesModule,
|
CompaniesModule,
|
||||||
|
BusinessesModule,
|
||||||
],
|
],
|
||||||
providers: [TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository, TicketMessageAttachmentsRepository],
|
providers: [TicketsService],
|
||||||
controllers: [TicketsController],
|
controllers: [TicketsController],
|
||||||
exports: [TicketsService],
|
exports: [TicketsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, Opt, Pr
|
|||||||
import { RefreshToken } from "./refresh-token.entity";
|
import { RefreshToken } from "./refresh-token.entity";
|
||||||
import { Role } from "./role.entity";
|
import { Role } from "./role.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { UserAnnouncement } from "../../announcements/entities/user-announcement.entity";
|
||||||
import { Company } from "../../companies/entities/company.entity";
|
import { Company } from "../../companies/entities/company.entity";
|
||||||
import { UserRepository } from "../repositories/user.repository";
|
import { UserRepository } from "../repositories/user.repository";
|
||||||
|
|
||||||
@@ -56,5 +57,8 @@ export class User extends BaseEntity {
|
|||||||
@OneToMany(() => Company, (company) => company.user)
|
@OneToMany(() => Company, (company) => company.user)
|
||||||
companies = new Collection<Company>(this);
|
companies = new Collection<Company>(this);
|
||||||
|
|
||||||
|
@OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.user)
|
||||||
|
userAnnouncements = new Collection<UserAnnouncement>(this);
|
||||||
|
|
||||||
[EntityRepositoryType]?: UserRepository;
|
[EntityRepositoryType]?: UserRepository;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,20 @@ export function numberFormat(number: number, locale: string = "fa-IR") {
|
|||||||
return Intl.NumberFormat(locale).format(number);
|
return Intl.NumberFormat(locale).format(number);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dateFormat(date: Date, locale: string = "fa-IR") {
|
export function dateFormat(date: Date | string | number, locale: string = "fa-IR") {
|
||||||
return new Intl.DateTimeFormat(locale).format(date);
|
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