feat: implement invoice processing and discount management
This commit is contained in:
@@ -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[];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user