feat: implement invoice processing and discount management
This commit is contained in:
@@ -579,6 +579,7 @@ export const enum DiscountMessage {
|
||||
UPDATED = "تخفیف با موفقیت بروزرسانی شد",
|
||||
DEACTIVATED = "تخفیف با موفقیت غیرفعال شد",
|
||||
TARGET_SERVICES_REQUIRED_FOR_DIRECT_DISCOUNT = "برای تخفیف مستقیم باید حداقل یک سرویس انتخاب شود",
|
||||
USER_ID_MUST_BE_A_UUID = "شناسه کاربر باید یک UUID معتبر باشد",
|
||||
}
|
||||
|
||||
export const enum EmailMessage {
|
||||
|
||||
@@ -31,7 +31,7 @@ export class CreateAnnouncementDto {
|
||||
@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: Date;
|
||||
publishAt: string;
|
||||
|
||||
@IsNotEmpty({ message: AnnouncementMessage.IMPORTANT_IS_REQUIRED })
|
||||
@IsBoolean({ message: AnnouncementMessage.IMPORTANT_MUST_BE_BOOLEAN })
|
||||
|
||||
@@ -72,6 +72,15 @@ export class CreateDiscountDto {
|
||||
})
|
||||
targetServices?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((o) => !o.direct)
|
||||
@IsUUID("4", { message: DiscountMessage.USER_ID_MUST_BE_A_UUID })
|
||||
@ApiPropertyOptional({
|
||||
description: "The ID of the user this discount is specific to. If not provided, the discount will be available to all users",
|
||||
example: "123e4567-e89b-12d3-a456-426614174000",
|
||||
})
|
||||
userId?: string;
|
||||
|
||||
// @IsString()
|
||||
// @IsOptional()
|
||||
// @ApiPropertyOptional({ description: "The code of the discount", example: "SUMMER2023" })
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm";
|
||||
import { Column, DeleteDateColumn, Entity, ManyToOne, OneToMany } from "typeorm";
|
||||
|
||||
import { UsageDiscount } from "./usage-discount.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
||||
import { Invoice } from "../../invoices/entities/invoice.entity";
|
||||
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { DiscountApplicationType } from "../enums/discount-application-type.enum";
|
||||
import { DiscountType } from "../enums/discount-type.enum";
|
||||
|
||||
@@ -40,7 +41,9 @@ export class Discount extends BaseEntity {
|
||||
@DeleteDateColumn({ nullable: true })
|
||||
deletedAt?: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => User, (user) => user.discounts, { nullable: true })
|
||||
user?: User | null;
|
||||
|
||||
@OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount)
|
||||
usages: UsageDiscount[];
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { Queue } from "bullmq";
|
||||
import dayjs from "dayjs";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { DataSource, QueryRunner } from "typeorm";
|
||||
import { DataSource, FindOptionsWhere, QueryRunner } from "typeorm";
|
||||
|
||||
import { CommonMessage, DiscountMessage } from "../../../common/enums/message.enum";
|
||||
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
|
||||
import { UsersService } from "../../users/providers/users.service";
|
||||
import { DISCOUNT } from "../constants";
|
||||
import { CreateDiscountDto } from "../DTO/create-discount.dto";
|
||||
import { SearchDiscountQueryDto } from "../DTO/search-discount-query.dto";
|
||||
@@ -27,6 +28,7 @@ export class DiscountsService {
|
||||
@InjectQueue(DISCOUNT.DISCOUNT_QUEUE_NAME) private readonly discountQueue: Queue,
|
||||
private readonly discountRepository: DiscountRepository,
|
||||
private readonly subscriptionService: SubscriptionsService,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
@@ -34,9 +36,7 @@ export class DiscountsService {
|
||||
// PUBLIC METHODS
|
||||
//======================================
|
||||
|
||||
/**
|
||||
* Create a new discount
|
||||
*/
|
||||
//************************************************************ */
|
||||
async create(createDiscountDto: CreateDiscountDto) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
@@ -63,9 +63,7 @@ export class DiscountsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all discounts for admin panel with pagination
|
||||
*/
|
||||
//************************************************************ */
|
||||
async getDiscountsForAdmin(queryDto: SearchDiscountQueryDto) {
|
||||
const [discounts, count] = await this.discountRepository.findDiscountForAdmin(queryDto);
|
||||
|
||||
@@ -76,9 +74,7 @@ export class DiscountsService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a discount by ID
|
||||
*/
|
||||
//************************************************************ */
|
||||
async getDiscountById(id: string) {
|
||||
const discount = await this.discountRepository.findById(id);
|
||||
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||
@@ -86,9 +82,7 @@ export class DiscountsService {
|
||||
return { discount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle discount active status
|
||||
*/
|
||||
//************************************************************ */
|
||||
async toggleDiscountStatus(id: string) {
|
||||
const discount = await this.discountRepository.findById(id);
|
||||
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||
@@ -102,9 +96,7 @@ export class DiscountsService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing discount
|
||||
*/
|
||||
//************************************************************ */
|
||||
async updateDiscount(id: string, updateDiscountDto: UpdateDiscountDto) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
@@ -176,9 +168,7 @@ export class DiscountsService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete a discount
|
||||
*/
|
||||
//************************************************************ */
|
||||
async softDeleteDiscount(id: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
try {
|
||||
@@ -211,9 +201,7 @@ export class DiscountsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a discount and update related subscription plans
|
||||
*/
|
||||
//************************************************************ */
|
||||
async deactivateDiscount(id: string, queryRunner: QueryRunner) {
|
||||
const discount = await queryRunner.manager.findOne(this.discountRepository.target, {
|
||||
where: { id, isActive: true },
|
||||
@@ -246,13 +234,18 @@ export class DiscountsService {
|
||||
// PRIVATE METHODS
|
||||
//======================================
|
||||
|
||||
/**
|
||||
* Create a discount entity and handle code generation or direct discount application
|
||||
*/
|
||||
//************************************************************ */
|
||||
|
||||
private async createDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) {
|
||||
const { userId } = createDiscountDto;
|
||||
if (userId) {
|
||||
await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner);
|
||||
}
|
||||
|
||||
const discount = queryRunner.manager.create(this.discountRepository.target, {
|
||||
...createDiscountDto,
|
||||
applicationType: createDiscountDto.direct ? DiscountApplicationType.DIRECT : DiscountApplicationType.CODE_BASED,
|
||||
user: !createDiscountDto.direct && userId ? { id: userId } : null,
|
||||
});
|
||||
|
||||
if (!createDiscountDto.direct) {
|
||||
@@ -269,9 +262,8 @@ export class DiscountsService {
|
||||
return discount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a direct discount to subscription plans
|
||||
*/
|
||||
//************************************************************ */
|
||||
|
||||
private async applyDirectDiscount(createDiscountDto: CreateDiscountDto, discount: Discount, queryRunner: QueryRunner) {
|
||||
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(
|
||||
createDiscountDto.targetServices ?? [],
|
||||
@@ -305,9 +297,7 @@ export class DiscountsService {
|
||||
await queryRunner.manager.save(this.discountRepository.target, discount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a direct discount's subscription plans
|
||||
*/
|
||||
//************************************************************ */
|
||||
private async updateDirectDiscount(updateDiscountDto: UpdateDiscountDto, discount: Discount, queryRunner: QueryRunner) {
|
||||
// Get all subscription plans for the target services
|
||||
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(
|
||||
@@ -380,9 +370,7 @@ export class DiscountsService {
|
||||
return subscriptionPlans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear direct discount from plans
|
||||
*/
|
||||
//************************************************************ */
|
||||
private async clearDirectDiscountFromPlans(discount: Discount, queryRunner: QueryRunner) {
|
||||
const subscriptionPlans = await queryRunner.manager.find(SubscriptionPlan, {
|
||||
where: { directDiscount: { id: discount.id } },
|
||||
@@ -396,9 +384,7 @@ export class DiscountsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a discount code for code-based discounts
|
||||
*/
|
||||
//************************************************************ */
|
||||
private async generateDiscountCode(discount: Discount, queryRunner: QueryRunner) {
|
||||
let attempts = 0;
|
||||
let code: string;
|
||||
@@ -415,26 +401,27 @@ export class DiscountsService {
|
||||
discount.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random discount code
|
||||
*/
|
||||
//************************************************************ */
|
||||
private async generateRandomCode(length: number = 4) {
|
||||
return randomBytes(length).toString("hex").toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a discount by code
|
||||
*/
|
||||
private async findDiscountByCode(code: string, queryRunner: QueryRunner) {
|
||||
//************************************************************ */
|
||||
private async findDiscountByCode(code: string, queryRunner: QueryRunner, userId?: string) {
|
||||
const where: FindOptionsWhere<Discount> = { code, isActive: true };
|
||||
|
||||
// If userId is provided, only return discounts that are either global or specific to this user
|
||||
if (userId) {
|
||||
where.user = { id: userId };
|
||||
}
|
||||
|
||||
return queryRunner.manager.findOne(this.discountRepository.target, {
|
||||
where: { code, isActive: true },
|
||||
where,
|
||||
withDeleted: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate discount value based on discount type
|
||||
*/
|
||||
//************************************************************ */
|
||||
private calculateDiscountValue(createDiscountDto: CreateDiscountDto): number {
|
||||
if (createDiscountDto.type === DiscountType.PERCENTAGE) return createDiscountDto.value;
|
||||
if (createDiscountDto.type === DiscountType.FIXED_AMOUNT) return createDiscountDto.value;
|
||||
@@ -442,9 +429,7 @@ export class DiscountsService {
|
||||
throw new BadRequestException(DiscountMessage.INVALID_DISCOUNT_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate discount start and end dates
|
||||
*/
|
||||
//************************************************************ */
|
||||
private async validateDates(startDate: string, endDate: string) {
|
||||
if (dayjs(startDate).isAfter(dayjs(endDate))) {
|
||||
throw new BadRequestException(DiscountMessage.START_DATE_MUST_BE_BEFORE_END_DATE);
|
||||
@@ -463,18 +448,14 @@ export class DiscountsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove discount deactivation job
|
||||
*/
|
||||
//************************************************************ */
|
||||
private async removeDiscountDeactivationJob(discountId: string) {
|
||||
const jobs = await this.discountQueue.getJobs(["delayed", "waiting", "active"]);
|
||||
const jobToRemove = jobs.find((job) => job.name === DISCOUNT.DISCOUNT_DEACTIVATE_JOB_NAME && job.data.discountId === discountId);
|
||||
if (jobToRemove) await jobToRemove.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add discount deactivation job
|
||||
*/
|
||||
//************************************************************ */
|
||||
private async addDiscountDeactivationJob(discountId: string, endDate: string) {
|
||||
return this.discountQueue.add(
|
||||
DISCOUNT.DISCOUNT_DEACTIVATE_JOB_NAME,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { FindOptionsWhere, Repository } from "typeorm";
|
||||
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { SearchDiscountQueryDto } from "../DTO/search-discount-query.dto";
|
||||
@@ -20,15 +20,27 @@ export class DiscountRepository extends Repository<Discount> {
|
||||
subscriptionPlans: {
|
||||
service: true,
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
withDeleted: false,
|
||||
});
|
||||
}
|
||||
|
||||
async findByCode(code: string) {
|
||||
async findByCode(code: string, userId?: string) {
|
||||
const where: FindOptionsWhere<Discount> = { code, isActive: true };
|
||||
|
||||
if (userId) {
|
||||
where.user = { id: userId };
|
||||
}
|
||||
|
||||
return this.findOne({
|
||||
where: { code, isActive: true },
|
||||
where,
|
||||
withDeleted: false,
|
||||
relations: {
|
||||
subscriptionPlans: {
|
||||
service: true,
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,11 +57,30 @@ export class DiscountRepository extends Repository<Discount> {
|
||||
async findDiscountForAdmin(queryDto: SearchDiscountQueryDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
const queryBuilder = this.createQueryBuilder("discount")
|
||||
.leftJoin("discount.subscriptionPlans", "plan")
|
||||
.addSelect(["plan.id", "plan.name", "plan.price", "plan.duration"])
|
||||
.leftJoin("plan.service", "service")
|
||||
.addSelect(["service.id", "service.name"])
|
||||
.where("discount.deletedAt IS NULL");
|
||||
.leftJoinAndSelect("discount.subscriptionPlans", "subscriptionPlans")
|
||||
.leftJoinAndSelect("subscriptionPlans.service", "service")
|
||||
.leftJoinAndSelect("discount.user", "user")
|
||||
.where("discount.deletedAt IS NULL")
|
||||
.select([
|
||||
"discount.id",
|
||||
"discount.title",
|
||||
"discount.type",
|
||||
"discount.code",
|
||||
"discount.value",
|
||||
"discount.startDate",
|
||||
"discount.endDate",
|
||||
"discount.isActive",
|
||||
"discount.applicationType",
|
||||
"discount.userId",
|
||||
"discount.createdAt",
|
||||
"subscriptionPlans.id",
|
||||
"service.id",
|
||||
"service.name",
|
||||
"user.id",
|
||||
"user.firstName",
|
||||
"user.lastName",
|
||||
"user.email",
|
||||
]);
|
||||
|
||||
if (queryDto.q) {
|
||||
queryBuilder.andWhere("discount.title ILIKE :query", { query: `%${queryDto.q}%` });
|
||||
@@ -61,8 +92,8 @@ export class DiscountRepository extends Repository<Discount> {
|
||||
async softDeleteDiscount(id: string) {
|
||||
return this.softDelete(id);
|
||||
}
|
||||
|
||||
async findDiscountForUser(queryDto: SearchDiscountQueryDto) {
|
||||
//************************************************************ */
|
||||
async findDiscountForUser(queryDto: SearchDiscountQueryDto, userId?: string) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
const queryBuilder = this.createQueryBuilder("discount")
|
||||
.where("discount.deletedAt IS NULL")
|
||||
@@ -70,6 +101,7 @@ export class DiscountRepository extends Repository<Discount> {
|
||||
.andWhere("discount.startDate <= :now", { now: new Date() })
|
||||
.andWhere("discount.endDate >= :now", { now: new Date() })
|
||||
.andWhere("discount.applicationType = :type", { type: DiscountApplicationType.CODE_BASED })
|
||||
.leftJoinAndSelect("discount.user", "user")
|
||||
.select([
|
||||
"discount.id",
|
||||
"discount.title",
|
||||
@@ -79,8 +111,17 @@ export class DiscountRepository extends Repository<Discount> {
|
||||
"discount.startDate",
|
||||
"discount.endDate",
|
||||
"discount.createdAt",
|
||||
"discount.userId",
|
||||
"user.id",
|
||||
"user.firstName",
|
||||
"user.lastName",
|
||||
"user.email",
|
||||
]);
|
||||
|
||||
if (userId) {
|
||||
queryBuilder.andWhere("(discount.userId = :userId OR discount.userId IS NULL)", { userId });
|
||||
}
|
||||
|
||||
if (queryDto.q) {
|
||||
queryBuilder.andWhere("discount.title ILIKE :query", { query: `%${queryDto.q}%` });
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
||||
import { LoggerService } from "../../logger/logger.service";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum";
|
||||
import { SupportPlan } from "../../support-plans/entities/support-plan.entity";
|
||||
import { UserSupportPlan } from "../../support-plans/entities/user-support-plan.entity";
|
||||
import { UserSupportPlanStatus } from "../../support-plans/enums/user-support-plan-status.enum";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { RoleEnum } from "../../users/enums/role.enum";
|
||||
import { INVOICE } from "../constants";
|
||||
@@ -210,6 +213,16 @@ export class InvoiceProcessor extends WorkerProcessor {
|
||||
invoiceId: invoice.numericId.toString(),
|
||||
});
|
||||
}
|
||||
if (invoice.items[0]?.supportPlan) {
|
||||
this.logger.log(
|
||||
`Invoice ${invoice.id} has been cancelled as it is more than 10 days overdue. Support Plan ${invoice.items[0].supportPlan.id} has been cancelled.`,
|
||||
);
|
||||
const userSupportPlan = invoice.items[0].supportPlan;
|
||||
userSupportPlan.status = UserSupportPlanStatus.INACTIVE;
|
||||
await queryRunner.manager.save(UserSupportPlan, userSupportPlan);
|
||||
|
||||
await this.applyFreeSupportPlan(invoice, queryRunner);
|
||||
}
|
||||
this.logger.log(`Invoice ${invoice.id} has been cancelled as it is more than 10 days overdue.`);
|
||||
}
|
||||
//********************************** */
|
||||
@@ -348,4 +361,39 @@ export class InvoiceProcessor extends WorkerProcessor {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
//********************************** */
|
||||
private async applyFreeSupportPlan(invoice: Invoice, queryRunner: QueryRunner) {
|
||||
const freeSupportPlan = await queryRunner.manager.findOne(SupportPlan, {
|
||||
where: { isFree: true },
|
||||
});
|
||||
|
||||
if (!freeSupportPlan) throw new Error(`No free support plan found`);
|
||||
|
||||
const startDate = dayjs().toDate();
|
||||
const endDate = dayjs().add(freeSupportPlan.duration, "day").toDate();
|
||||
|
||||
const userSupportPlan = queryRunner.manager.create(UserSupportPlan, {
|
||||
user: invoice.user,
|
||||
supportPlan: freeSupportPlan,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
|
||||
await queryRunner.manager.save(UserSupportPlan, userSupportPlan);
|
||||
|
||||
const invoiceDueDate = dayjs(userSupportPlan.startDate).add(INVOICE.DUEDATE, "day").toDate();
|
||||
|
||||
const newInvoice = await this.invoicesService.createInvoiceForSupportPlan(
|
||||
invoice.user,
|
||||
freeSupportPlan,
|
||||
userSupportPlan,
|
||||
invoiceDueDate,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
userSupportPlan.status = UserSupportPlanStatus.ACTIVE;
|
||||
await queryRunner.manager.save(UserSupportPlan, userSupportPlan);
|
||||
await this.invoicesService.payInvoice(newInvoice.id, invoice.user.id, queryRunner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ export class SupportPlansService {
|
||||
async getUserSupportPlanOfUser(userId: string) {
|
||||
const userSupportPlan = await this.userSupportPlanRepo.findOne({
|
||||
where: { user: { id: userId }, status: UserSupportPlanStatus.ACTIVE },
|
||||
relations: { supportPlan: true },
|
||||
relations: { supportPlan: { features: true } },
|
||||
});
|
||||
|
||||
return { userSupportPlan };
|
||||
|
||||
@@ -143,16 +143,14 @@ export class TicketsService {
|
||||
|
||||
async createTicket(createDto: CreateTicketDto, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const [user, ticketCategory] = await Promise.all([
|
||||
this.usersService.findOneByIdWithQueryRunner(userId, queryRunner),
|
||||
queryRunner.manager.findOne(TicketCategory, {
|
||||
where: { id: createDto.categoryId },
|
||||
relations: { group: { users: true } },
|
||||
}),
|
||||
queryRunner.manager.findOne(TicketCategory, { where: { id: createDto.categoryId }, relations: { group: { users: true } } }),
|
||||
]);
|
||||
|
||||
if (!ticketCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
||||
@@ -161,16 +159,11 @@ export class TicketsService {
|
||||
|
||||
let service = null;
|
||||
if (createDto.danakServiceId) {
|
||||
service = await queryRunner.manager.findOneBy(DanakService, {
|
||||
id: createDto.danakServiceId,
|
||||
});
|
||||
service = await queryRunner.manager.findOneBy(DanakService, { id: createDto.danakServiceId });
|
||||
if (!service) throw new BadRequestException(TicketMessageEnum.SERVICE_NOT_FOUND);
|
||||
}
|
||||
|
||||
const ticketMessage: Partial<TicketMessage> = {
|
||||
content: createDto.message,
|
||||
author: user,
|
||||
};
|
||||
const ticketMessage: Partial<TicketMessage> = { content: createDto.message, author: user };
|
||||
|
||||
if (createDto.attachmentUrls?.length) {
|
||||
ticketMessage.attachments = createDto.attachmentUrls.map((url) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { BlogComment } from "../../blogs/entities/blog-comment.entity";
|
||||
import { Blog } from "../../blogs/entities/blog.entity";
|
||||
import { Criticism } from "../../criticisms/entities/criticism.entity";
|
||||
import { DanakServiceReview } from "../../danak-services/entities/danak-service-review.entity";
|
||||
import { Discount } from "../../discounts/entities/discount.entity";
|
||||
import { Invoice } from "../../invoices/entities/invoice.entity";
|
||||
import { LearningProgress } from "../../learnings/entities/learning-progress.entity";
|
||||
import { Notification } from "../../notifications/entities/notification.entity";
|
||||
@@ -125,6 +126,9 @@ export class User extends BaseEntity {
|
||||
// @ManyToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.users)
|
||||
// usageDiscounts: UsageDiscount[];
|
||||
|
||||
@OneToMany(() => Discount, (discount) => discount.user)
|
||||
discounts: Discount[];
|
||||
|
||||
@OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.user)
|
||||
userAnnouncements: UserAnnouncement[];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user