feat: implement invoice processing and discount management

This commit is contained in:
mahyargdz
2025-05-07 14:45:44 +03:30
parent 9cd28e8130
commit 9faa75b049
10 changed files with 165 additions and 85 deletions
+1
View File
@@ -579,6 +579,7 @@ export const enum DiscountMessage {
UPDATED = "تخفیف با موفقیت بروزرسانی شد", UPDATED = "تخفیف با موفقیت بروزرسانی شد",
DEACTIVATED = "تخفیف با موفقیت غیرفعال شد", DEACTIVATED = "تخفیف با موفقیت غیرفعال شد",
TARGET_SERVICES_REQUIRED_FOR_DIRECT_DISCOUNT = "برای تخفیف مستقیم باید حداقل یک سرویس انتخاب شود", TARGET_SERVICES_REQUIRED_FOR_DIRECT_DISCOUNT = "برای تخفیف مستقیم باید حداقل یک سرویس انتخاب شود",
USER_ID_MUST_BE_A_UUID = "شناسه کاربر باید یک UUID معتبر باشد",
} }
export const enum EmailMessage { export const enum EmailMessage {
@@ -31,7 +31,7 @@ export class CreateAnnouncementDto {
@IsNotEmpty({ message: AnnouncementMessage.PUBLISH_AT_IS_REQUIRED }) @IsNotEmpty({ message: AnnouncementMessage.PUBLISH_AT_IS_REQUIRED })
@IsDateString({}, { message: AnnouncementMessage.PUBLISH_AT_MUST_BE_DATE }) @IsDateString({}, { message: AnnouncementMessage.PUBLISH_AT_MUST_BE_DATE })
@ApiProperty({ description: "Publish date of the announcement", example: "2023-10-01T10:00:00Z" }) @ApiProperty({ description: "Publish date of the announcement", example: "2023-10-01T10:00:00Z" })
publishAt: Date; publishAt: string;
@IsNotEmpty({ message: AnnouncementMessage.IMPORTANT_IS_REQUIRED }) @IsNotEmpty({ message: AnnouncementMessage.IMPORTANT_IS_REQUIRED })
@IsBoolean({ message: AnnouncementMessage.IMPORTANT_MUST_BE_BOOLEAN }) @IsBoolean({ message: AnnouncementMessage.IMPORTANT_MUST_BE_BOOLEAN })
@@ -72,6 +72,15 @@ export class CreateDiscountDto {
}) })
targetServices?: string[]; 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() // @IsString()
// @IsOptional() // @IsOptional()
// @ApiPropertyOptional({ description: "The code of the discount", example: "SUMMER2023" }) // @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 { UsageDiscount } from "./usage-discount.entity";
import { BaseEntity } from "../../../common/entities/base.entity"; import { BaseEntity } from "../../../common/entities/base.entity";
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
import { Invoice } from "../../invoices/entities/invoice.entity"; import { Invoice } from "../../invoices/entities/invoice.entity";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { User } from "../../users/entities/user.entity";
import { DiscountApplicationType } from "../enums/discount-application-type.enum"; import { DiscountApplicationType } from "../enums/discount-application-type.enum";
import { DiscountType } from "../enums/discount-type.enum"; import { DiscountType } from "../enums/discount-type.enum";
@@ -40,7 +41,9 @@ export class Discount extends BaseEntity {
@DeleteDateColumn({ nullable: true }) @DeleteDateColumn({ nullable: true })
deletedAt?: Date; deletedAt?: Date;
// Relations @ManyToOne(() => User, (user) => user.discounts, { nullable: true })
user?: User | null;
@OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount) @OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount)
usages: UsageDiscount[]; usages: UsageDiscount[];
@@ -5,11 +5,12 @@ import { BadRequestException, Injectable } from "@nestjs/common";
import { Queue } from "bullmq"; import { Queue } from "bullmq";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Decimal } from "decimal.js"; 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 { CommonMessage, DiscountMessage } from "../../../common/enums/message.enum";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service"; import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
import { UsersService } from "../../users/providers/users.service";
import { DISCOUNT } from "../constants"; import { DISCOUNT } from "../constants";
import { CreateDiscountDto } from "../DTO/create-discount.dto"; import { CreateDiscountDto } from "../DTO/create-discount.dto";
import { SearchDiscountQueryDto } from "../DTO/search-discount-query.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, @InjectQueue(DISCOUNT.DISCOUNT_QUEUE_NAME) private readonly discountQueue: Queue,
private readonly discountRepository: DiscountRepository, private readonly discountRepository: DiscountRepository,
private readonly subscriptionService: SubscriptionsService, private readonly subscriptionService: SubscriptionsService,
private readonly usersService: UsersService,
private readonly dataSource: DataSource, private readonly dataSource: DataSource,
) {} ) {}
@@ -34,9 +36,7 @@ export class DiscountsService {
// PUBLIC METHODS // PUBLIC METHODS
//====================================== //======================================
/** //************************************************************ */
* Create a new discount
*/
async create(createDiscountDto: CreateDiscountDto) { async create(createDiscountDto: CreateDiscountDto) {
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
@@ -63,9 +63,7 @@ export class DiscountsService {
} }
} }
/** //************************************************************ */
* Get all discounts for admin panel with pagination
*/
async getDiscountsForAdmin(queryDto: SearchDiscountQueryDto) { async getDiscountsForAdmin(queryDto: SearchDiscountQueryDto) {
const [discounts, count] = await this.discountRepository.findDiscountForAdmin(queryDto); const [discounts, count] = await this.discountRepository.findDiscountForAdmin(queryDto);
@@ -76,9 +74,7 @@ export class DiscountsService {
}; };
} }
/** //************************************************************ */
* Get a discount by ID
*/
async getDiscountById(id: string) { async getDiscountById(id: string) {
const discount = await this.discountRepository.findById(id); const discount = await this.discountRepository.findById(id);
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND); if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
@@ -86,9 +82,7 @@ export class DiscountsService {
return { discount }; return { discount };
} }
/** //************************************************************ */
* Toggle discount active status
*/
async toggleDiscountStatus(id: string) { async toggleDiscountStatus(id: string) {
const discount = await this.discountRepository.findById(id); const discount = await this.discountRepository.findById(id);
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND); 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) { async updateDiscount(id: string, updateDiscountDto: UpdateDiscountDto) {
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
@@ -176,9 +168,7 @@ export class DiscountsService {
}; };
} }
/** //************************************************************ */
* Soft delete a discount
*/
async softDeleteDiscount(id: string) { async softDeleteDiscount(id: string) {
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
try { try {
@@ -211,9 +201,7 @@ export class DiscountsService {
} }
} }
/** //************************************************************ */
* Deactivate a discount and update related subscription plans
*/
async deactivateDiscount(id: string, queryRunner: QueryRunner) { async deactivateDiscount(id: string, queryRunner: QueryRunner) {
const discount = await queryRunner.manager.findOne(this.discountRepository.target, { const discount = await queryRunner.manager.findOne(this.discountRepository.target, {
where: { id, isActive: true }, where: { id, isActive: true },
@@ -246,13 +234,18 @@ export class DiscountsService {
// PRIVATE METHODS // PRIVATE METHODS
//====================================== //======================================
/** //************************************************************ */
* Create a discount entity and handle code generation or direct discount application
*/
private async createDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) { 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, { const discount = queryRunner.manager.create(this.discountRepository.target, {
...createDiscountDto, ...createDiscountDto,
applicationType: createDiscountDto.direct ? DiscountApplicationType.DIRECT : DiscountApplicationType.CODE_BASED, applicationType: createDiscountDto.direct ? DiscountApplicationType.DIRECT : DiscountApplicationType.CODE_BASED,
user: !createDiscountDto.direct && userId ? { id: userId } : null,
}); });
if (!createDiscountDto.direct) { if (!createDiscountDto.direct) {
@@ -269,9 +262,8 @@ export class DiscountsService {
return discount; return discount;
} }
/** //************************************************************ */
* Apply a direct discount to subscription plans
*/
private async applyDirectDiscount(createDiscountDto: CreateDiscountDto, discount: Discount, queryRunner: QueryRunner) { private async applyDirectDiscount(createDiscountDto: CreateDiscountDto, discount: Discount, queryRunner: QueryRunner) {
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions( const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(
createDiscountDto.targetServices ?? [], createDiscountDto.targetServices ?? [],
@@ -305,9 +297,7 @@ export class DiscountsService {
await queryRunner.manager.save(this.discountRepository.target, discount); await queryRunner.manager.save(this.discountRepository.target, discount);
} }
/** //************************************************************ */
* Update a direct discount's subscription plans
*/
private async updateDirectDiscount(updateDiscountDto: UpdateDiscountDto, discount: Discount, queryRunner: QueryRunner) { private async updateDirectDiscount(updateDiscountDto: UpdateDiscountDto, discount: Discount, queryRunner: QueryRunner) {
// Get all subscription plans for the target services // Get all subscription plans for the target services
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions( const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(
@@ -380,9 +370,7 @@ export class DiscountsService {
return subscriptionPlans; return subscriptionPlans;
} }
/** //************************************************************ */
* Clear direct discount from plans
*/
private async clearDirectDiscountFromPlans(discount: Discount, queryRunner: QueryRunner) { private async clearDirectDiscountFromPlans(discount: Discount, queryRunner: QueryRunner) {
const subscriptionPlans = await queryRunner.manager.find(SubscriptionPlan, { const subscriptionPlans = await queryRunner.manager.find(SubscriptionPlan, {
where: { directDiscount: { id: discount.id } }, 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) { private async generateDiscountCode(discount: Discount, queryRunner: QueryRunner) {
let attempts = 0; let attempts = 0;
let code: string; let code: string;
@@ -415,26 +401,27 @@ export class DiscountsService {
discount.code = code; discount.code = code;
} }
/** //************************************************************ */
* Generate a random discount code
*/
private async generateRandomCode(length: number = 4) { private async generateRandomCode(length: number = 4) {
return randomBytes(length).toString("hex").toUpperCase(); return randomBytes(length).toString("hex").toUpperCase();
} }
/** //************************************************************ */
* Find a discount by code private async findDiscountByCode(code: string, queryRunner: QueryRunner, userId?: string) {
*/ const where: FindOptionsWhere<Discount> = { code, isActive: true };
private async findDiscountByCode(code: string, queryRunner: QueryRunner) {
// 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, { return queryRunner.manager.findOne(this.discountRepository.target, {
where: { code, isActive: true }, where,
withDeleted: false, withDeleted: false,
}); });
} }
/** //************************************************************ */
* Calculate discount value based on discount type
*/
private calculateDiscountValue(createDiscountDto: CreateDiscountDto): number { private calculateDiscountValue(createDiscountDto: CreateDiscountDto): number {
if (createDiscountDto.type === DiscountType.PERCENTAGE) return createDiscountDto.value; if (createDiscountDto.type === DiscountType.PERCENTAGE) return createDiscountDto.value;
if (createDiscountDto.type === DiscountType.FIXED_AMOUNT) 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); throw new BadRequestException(DiscountMessage.INVALID_DISCOUNT_TYPE);
} }
/** //************************************************************ */
* Validate discount start and end dates
*/
private async validateDates(startDate: string, endDate: string) { private async validateDates(startDate: string, endDate: string) {
if (dayjs(startDate).isAfter(dayjs(endDate))) { if (dayjs(startDate).isAfter(dayjs(endDate))) {
throw new BadRequestException(DiscountMessage.START_DATE_MUST_BE_BEFORE_END_DATE); 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) { private async removeDiscountDeactivationJob(discountId: string) {
const jobs = await this.discountQueue.getJobs(["delayed", "waiting", "active"]); 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); const jobToRemove = jobs.find((job) => job.name === DISCOUNT.DISCOUNT_DEACTIVATE_JOB_NAME && job.data.discountId === discountId);
if (jobToRemove) await jobToRemove.remove(); if (jobToRemove) await jobToRemove.remove();
} }
/** //************************************************************ */
* Add discount deactivation job
*/
private async addDiscountDeactivationJob(discountId: string, endDate: string) { private async addDiscountDeactivationJob(discountId: string, endDate: string) {
return this.discountQueue.add( return this.discountQueue.add(
DISCOUNT.DISCOUNT_DEACTIVATE_JOB_NAME, DISCOUNT.DISCOUNT_DEACTIVATE_JOB_NAME,
@@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm"; import { FindOptionsWhere, Repository } from "typeorm";
import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { SearchDiscountQueryDto } from "../DTO/search-discount-query.dto"; import { SearchDiscountQueryDto } from "../DTO/search-discount-query.dto";
@@ -20,15 +20,27 @@ export class DiscountRepository extends Repository<Discount> {
subscriptionPlans: { subscriptionPlans: {
service: true, 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({ return this.findOne({
where: { code, isActive: true }, where,
withDeleted: false, withDeleted: false,
relations: {
subscriptionPlans: {
service: true,
},
user: true,
},
}); });
} }
@@ -45,11 +57,30 @@ export class DiscountRepository extends Repository<Discount> {
async findDiscountForAdmin(queryDto: SearchDiscountQueryDto) { async findDiscountForAdmin(queryDto: SearchDiscountQueryDto) {
const { limit, skip } = PaginationUtils(queryDto); const { limit, skip } = PaginationUtils(queryDto);
const queryBuilder = this.createQueryBuilder("discount") const queryBuilder = this.createQueryBuilder("discount")
.leftJoin("discount.subscriptionPlans", "plan") .leftJoinAndSelect("discount.subscriptionPlans", "subscriptionPlans")
.addSelect(["plan.id", "plan.name", "plan.price", "plan.duration"]) .leftJoinAndSelect("subscriptionPlans.service", "service")
.leftJoin("plan.service", "service") .leftJoinAndSelect("discount.user", "user")
.addSelect(["service.id", "service.name"]) .where("discount.deletedAt IS NULL")
.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) { if (queryDto.q) {
queryBuilder.andWhere("discount.title ILIKE :query", { query: `%${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) { async softDeleteDiscount(id: string) {
return this.softDelete(id); return this.softDelete(id);
} }
//************************************************************ */
async findDiscountForUser(queryDto: SearchDiscountQueryDto) { async findDiscountForUser(queryDto: SearchDiscountQueryDto, userId?: string) {
const { limit, skip } = PaginationUtils(queryDto); const { limit, skip } = PaginationUtils(queryDto);
const queryBuilder = this.createQueryBuilder("discount") const queryBuilder = this.createQueryBuilder("discount")
.where("discount.deletedAt IS NULL") .where("discount.deletedAt IS NULL")
@@ -70,6 +101,7 @@ export class DiscountRepository extends Repository<Discount> {
.andWhere("discount.startDate <= :now", { now: new Date() }) .andWhere("discount.startDate <= :now", { now: new Date() })
.andWhere("discount.endDate >= :now", { now: new Date() }) .andWhere("discount.endDate >= :now", { now: new Date() })
.andWhere("discount.applicationType = :type", { type: DiscountApplicationType.CODE_BASED }) .andWhere("discount.applicationType = :type", { type: DiscountApplicationType.CODE_BASED })
.leftJoinAndSelect("discount.user", "user")
.select([ .select([
"discount.id", "discount.id",
"discount.title", "discount.title",
@@ -79,8 +111,17 @@ export class DiscountRepository extends Repository<Discount> {
"discount.startDate", "discount.startDate",
"discount.endDate", "discount.endDate",
"discount.createdAt", "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) { if (queryDto.q) {
queryBuilder.andWhere("discount.title ILIKE :query", { query: `%${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 { LoggerService } from "../../logger/logger.service";
import { NotificationQueue } from "../../notifications/queue/notification.queue"; import { NotificationQueue } from "../../notifications/queue/notification.queue";
import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum"; 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 { User } from "../../users/entities/user.entity";
import { RoleEnum } from "../../users/enums/role.enum"; import { RoleEnum } from "../../users/enums/role.enum";
import { INVOICE } from "../constants"; import { INVOICE } from "../constants";
@@ -210,6 +213,16 @@ export class InvoiceProcessor extends WorkerProcessor {
invoiceId: invoice.numericId.toString(), 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.`); 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(); 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) { async getUserSupportPlanOfUser(userId: string) {
const userSupportPlan = await this.userSupportPlanRepo.findOne({ const userSupportPlan = await this.userSupportPlanRepo.findOne({
where: { user: { id: userId }, status: UserSupportPlanStatus.ACTIVE }, where: { user: { id: userId }, status: UserSupportPlanStatus.ACTIVE },
relations: { supportPlan: true }, relations: { supportPlan: { features: true } },
}); });
return { userSupportPlan }; return { userSupportPlan };
@@ -143,16 +143,14 @@ export class TicketsService {
async createTicket(createDto: CreateTicketDto, userId: string) { async createTicket(createDto: CreateTicketDto, userId: string) {
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try { try {
await queryRunner.connect();
await queryRunner.startTransaction();
const [user, ticketCategory] = await Promise.all([ const [user, ticketCategory] = await Promise.all([
this.usersService.findOneByIdWithQueryRunner(userId, queryRunner), this.usersService.findOneByIdWithQueryRunner(userId, queryRunner),
queryRunner.manager.findOne(TicketCategory, { queryRunner.manager.findOne(TicketCategory, { where: { id: createDto.categoryId }, relations: { group: { users: true } } }),
where: { id: createDto.categoryId },
relations: { group: { users: true } },
}),
]); ]);
if (!ticketCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND); if (!ticketCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
@@ -161,16 +159,11 @@ export class TicketsService {
let service = null; let service = null;
if (createDto.danakServiceId) { if (createDto.danakServiceId) {
service = await queryRunner.manager.findOneBy(DanakService, { service = await queryRunner.manager.findOneBy(DanakService, { id: createDto.danakServiceId });
id: createDto.danakServiceId,
});
if (!service) throw new BadRequestException(TicketMessageEnum.SERVICE_NOT_FOUND); if (!service) throw new BadRequestException(TicketMessageEnum.SERVICE_NOT_FOUND);
} }
const ticketMessage: Partial<TicketMessage> = { const ticketMessage: Partial<TicketMessage> = { content: createDto.message, author: user };
content: createDto.message,
author: user,
};
if (createDto.attachmentUrls?.length) { if (createDto.attachmentUrls?.length) {
ticketMessage.attachments = createDto.attachmentUrls.map((url) => { 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 { Blog } from "../../blogs/entities/blog.entity";
import { Criticism } from "../../criticisms/entities/criticism.entity"; import { Criticism } from "../../criticisms/entities/criticism.entity";
import { DanakServiceReview } from "../../danak-services/entities/danak-service-review.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 { Invoice } from "../../invoices/entities/invoice.entity";
import { LearningProgress } from "../../learnings/entities/learning-progress.entity"; import { LearningProgress } from "../../learnings/entities/learning-progress.entity";
import { Notification } from "../../notifications/entities/notification.entity"; import { Notification } from "../../notifications/entities/notification.entity";
@@ -125,6 +126,9 @@ export class User extends BaseEntity {
// @ManyToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.users) // @ManyToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.users)
// usageDiscounts: UsageDiscount[]; // usageDiscounts: UsageDiscount[];
@OneToMany(() => Discount, (discount) => discount.user)
discounts: Discount[];
@OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.user) @OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.user)
userAnnouncements: UserAnnouncement[]; userAnnouncements: UserAnnouncement[];