update: the danak services module and add new route to fetch the danak service

This commit is contained in:
mahyargdz
2025-04-16 15:28:45 +03:30
parent fb8f16deb4
commit 999ce9cee2
30 changed files with 713 additions and 465 deletions
+2
View File
@@ -25,6 +25,7 @@ import { ContactUsModule } from "./modules/contact-us/contact-us.module";
import { CriticismModule } from "./modules/criticisms/criticisms.module";
import { DanakServicesModule } from "./modules/danak-services/danak-services.module";
import { DashboardModule } from "./modules/dashboards/dashboard.module";
import { DiscountsModule } from "./modules/discounts/discounts.module";
import { InvoicesModule } from "./modules/invoices/invoices.module";
import { LandingModule } from "./modules/landing/landing.module";
import { LearningModule } from "./modules/learnings/learning.module";
@@ -72,6 +73,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module";
// LoggerModule,
LandingModule,
ReferralsModule,
DiscountsModule,
],
controllers: [],
})
+14 -30
View File
@@ -484,36 +484,20 @@ export const enum LearningMessage {
}
export const enum DiscountMessage {
CAN_NOT_APPLICABLE = "این کد تخفیف برای این صورت حساب قابل اعمال نیست",
TYPE_REQUIRED = "وارد کردن نوع تخفیف الزامی است",
TYPE_INVALID = "نوع تخفیف نامعتبر است",
CALCULATION_TYPE_REQUIRED = "وارد کردن نوع محاسبه تخفیف الزامی است",
CALCULATION_TYPE_INVALID = "نوع محاسبه تخفیف نامعتبر است",
AMOUNT_REQUIRED = "وارد کردن مقدار تخفیف الزامی است",
AMOUNT_INVALID = "مقدار تخفیف باید عدد باشد",
TITLE_REQUIRED = "وارد کردن مقدار عنوان الزامی است",
TITLE_INVALID = "مقدار عنوان باید یک رشته باشد",
CODE_REQUIRED = "وارد کردن کد تخفیف الزامی است",
CODE_INVALID = "کد تخفیف نامعتبر است",
START_DATE_REQUIRED = ارد کردن تاریخ شروع الزامی است",
START_DATE_INVALID = اریخ شروع نامعتبر است",
END_DATE_REQUIRED = "وارد کردن تاریخ پایان الزامی است",
END_DATE_INVALID = "تاریخ پایان نامعتبر است",
SERVICES_INVALID = "لیست خدمات باید آرایه‌ای از ایدی باشد",
SERVICES_DUPLICATE = "خدمات نباید تکراری باشند",
SERVICES_UUID_INVALID = "ایدی سرویس معتبرنیست",
USER_UUID_INVALID = "ایدی کاربر معتبرنیست",
USER_DUPLICATE = "کاربر نباید تکراری باشند",
USER_INVALID = "لیست کاربرا باید آرایه‌ای از ایدی باشد",
NOT_FOUND = "کد تخفیف با این مشخصات یافت نشد",
CODE_EXIST = "کد تخفیف قبلا ایجاد شده است",
INVOICE_ID_REQUIRED = "شناسه صورت حساب نمی‌تواند خالی باشد",
INVOICE_ID_STRING = "شناسه صورت حساب باید یک رشته باشد",
DISCOUNT_CODE_REQUIRED = "کد تخفیف نمی‌تواند خالی باشد",
DISCOUNT_CODE_STRING = "کد تخفیف باید یک رشته باشد",
DISCOUNT_NOT_FOUND = "کد تخفیف یافت نشد یا غیرفعال است",
DISCOUNT_NOT_APPLICABLE = "این کد تخفیف برای هیچ یک از پلن‌های اشتراک در صورت حساب قابل اعمال نیست",
ALREADY_USED = "این کد تخفیف قبلا استفاده شده است",
TITLE_STRING = "عنوان باید یک رشته باشد",
TITLE_REQUIRED = "عنوان الزامی است",
DISCOUNT_TYPE_REQUIRED = "نوع تخفیف الزامی است",
INVALID_DISCOUNT_TYPE = "نوع تخفیف نامعتبر است",
VALUE_MUST_BE_A_NUMBER = "قیمت باید یک عدد صحیح باشد",
VALUE_MUST_BE_AT_LEAST_0 = "قیمت باید حداقل ۰ باشد",
VALUE_MUST_NOT_EXCEED_100 = "قیمت باید حداکثر ۱۰۰ باشد",
START_DATE_MUST_BE_A_VALID_DATE_STRING = "تاریخ شروع باید یک تاریخ معتبر باشد",
START_DATE_IS_REQUIRED = "تاریخ شروع الزامی است",
END_DATE_MUST_BE_A_VALID_DATE_STRING = "تاریخ پایان باید یک تاریخ معتبر باشد",
END_DATE_IS_REQUIRED = "تاریخ پایان الزامی است",
IS_ACTIVE_MUST_BE_A_BOOLEAN = ضعیت فعال بودن باید یک بولین باشد",
TARGET_PRODUCTS_MUST_BE_AN_ARRAY = عداد محصولات باید یک آرایه باشد",
EACH_TARGET_PRODUCT_MUST_BE_A_STRING = "هر محصول باید یک رشته باشد",
}
export const enum EmailMessage {
+1 -1
View File
@@ -13,7 +13,7 @@ export function databaseConfigs(): TypeOrmModuleAsyncOptions {
username: configService.getOrThrow<string>("DB_USER"),
password: configService.getOrThrow<string>("DB_PASS"),
autoLoadEntities: true,
synchronize: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
synchronize: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : false,
logging: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
migrationsTableName: "typeorm_migrations",
migrationsRun: false,
@@ -141,6 +141,12 @@ export class DanakServicesController {
return this.danakServicesService.getDanakServiceByIdWithSubs(paramDto.id, isAdmin, userId);
}
@ApiOperation({ summary: "get danakService by id public" })
@Get(":id/public")
getDanakServiceByIdPublic(@Param() paramDto: ParamDto) {
return this.danakServicesService.getDanakServiceByIdPublic(paramDto.id);
}
@ApiOperation({ summary: "update danak service" })
@AuthGuards()
@PermissionsDec(PermissionEnum.SERVICES)
@@ -6,6 +6,7 @@ import { DanakServiceReview } from "./danak-service-review.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { Ads } from "../../ads/entities/ads.entity";
import { Announcement } from "../../announcements/entities/announcement.entity";
import { DiscountService } from "../../discounts/entities/discount-product.entity";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { Ticket } from "../../tickets/entities/ticket.entity";
import { ServicesLanguage } from "../enums/services-language.enum";
@@ -76,6 +77,9 @@ export class DanakService extends BaseEntity {
@OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.service)
reviews: DanakServiceReview[];
@OneToMany(() => DiscountService, (discountService) => discountService.danakService)
discountServices: DiscountService[];
@DeleteDateColumn()
deletedAt?: Date;
}
@@ -328,22 +328,7 @@ export class DanakServicesService {
/******************************************** */
async searchServices(queryDto: DanakServicesGlobalSearchDto) {
const servicesQuery = this.danakServicesRepository
.createQueryBuilder("service")
.leftJoin("service.category", "category")
.leftJoin("service.subscriptionPlans", "subscriptionPlans")
.select(["service.id", "service.name", "service.description", "service.icon", "category.id", "category.title"])
.where("service.isActive = :isActive", { isActive: true })
.andWhere("service.deletedAt IS NULL")
.andWhere("category.isActive = :categoryActive", { categoryActive: true })
.andWhere("category.deletedAt IS NULL")
.andWhere("subscriptionPlans.isActive = :planActive", { planActive: true });
if (queryDto.q) {
servicesQuery.andWhere("service.name ILIKE :searchQuery", { searchQuery: `%${queryDto.q}%` });
}
const services = await servicesQuery.getMany();
const services = await this.danakServicesRepository.searchServices(queryDto);
return {
services,
@@ -381,57 +366,11 @@ export class DanakServicesService {
/******************************************** */
async getDanakServiceByIdWithSubs(serviceId: string, isAdmin: boolean, userId: string) {
const serviceQueryBuilder = this.danakServicesRepository
.createQueryBuilder("service")
.where("service.deletedAt IS NULL")
.leftJoin("service.category", "category")
.addSelect(["category.id", "category.title"])
.leftJoinAndSelect("service.images", "images")
.leftJoinAndSelect("service.subscriptionPlans", "subscriptionPlans")
.leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
.leftJoin("reviews.user", "user")
.addSelect([
"reviews.id",
"reviews.comment",
"reviews.rating",
"reviews.title",
"reviews.createdAt",
"reviews.status",
"user.id",
"user.firstName",
"user.lastName",
"user.profilePic",
])
// .loadRelationCountAndMap("service.reviewCount", "service.reviews")
// .addSelect(
// (subQuery) =>
// subQuery
// .select("CAST(AVG(rating) AS DECIMAL(10,2))", "averageRating")
// .from(DanakServiceReview, "r")
// .where("r.serviceId = service.id")
// .andWhere("r.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED }),
// "averageRating",
// )
.andWhere("service.id = :serviceId", { serviceId })
.groupBy("service.id,category.id, images.id, subscriptionPlans.id, reviews.id, user.id");
const danakService = await this.danakServicesRepository.getDanakServiceById(serviceId, isAdmin);
if (!isAdmin) {
serviceQueryBuilder
.andWhere("service.isActive = :isActive", { isActive: true })
.andWhere("subscriptionPlans.isActive = :planActive", { planActive: true });
}
const danakService = await serviceQueryBuilder.getOne();
if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID);
const averageRatingResult = await this.danakServiceReviewRepository
.createQueryBuilder("r")
.select("CAST(AVG(r.rating) AS DECIMAL(10,2))", "averageRating")
.where("r.serviceId = :serviceId", { serviceId })
.andWhere("r.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
.getRawOne();
const averageRating = parseFloat(averageRatingResult?.averageRating) || 0;
const averageRating = await this.danakServiceReviewRepository.getAverageRating(danakService.id);
const hasSubscription = await this.checkUserPurchasedService(userId, serviceId);
@@ -443,25 +382,17 @@ export class DanakServicesService {
}
/******************************************** */
async getDanakServiceByIdPublic(serviceId: string) {
const danakService = await this.danakServicesRepository.getDanakServiceById(serviceId, false);
if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID);
return { danakService };
}
/******************************************** */
async getServiceUsers(serviceId: string) {
await this.findServiceById(serviceId);
const users = await this.danakServicesRepository
.createQueryBuilder("service")
.innerJoin("service.subscriptionPlans", "subscriptionPlans")
.innerJoin("subscriptionPlans.userSubscriptions", "userSubscriptions")
.innerJoin("user", "user", "userSubscriptions.userId = user.id")
.select([
"DISTINCT user.id AS id",
"user.firstName AS firstName",
"user.lastName AS lastName",
"user.email AS email",
"user.profilePic AS profilePic",
])
.where("service.id = :serviceId", { serviceId })
.andWhere("userSubscriptions.status = :status", { status: SubscriptionStatus.ACTIVE })
.andWhere("userSubscriptions.endDate > NOW()")
.getRawMany();
const users = await this.danakServicesRepository.getUsersByService(serviceId);
return { users };
}
@@ -469,31 +400,7 @@ export class DanakServicesService {
/******************************************** */
async getDanakServiceReviews(queryDto: DanakServiceReviewQueryDto) {
const { limit, skip } = PaginationUtils(queryDto);
const reviewsQueryBuilder = this.danakServiceReviewRepository
.createQueryBuilder("review")
.leftJoinAndSelect("review.user", "user")
.leftJoinAndSelect("review.service", "service")
.select([
"review.id",
"review.title",
"review.comment",
"review.rating",
"review.status",
"review.createdAt",
"user.id",
"user.firstName",
"user.lastName",
"user.profilePic",
"service.id",
"service.name",
])
.orderBy("review.createdAt", "DESC")
.skip(skip)
.take(limit);
const [reviews, count] = await reviewsQueryBuilder.getManyAndCount();
const [reviews, count] = await this.danakServiceReviewRepository.getDanakServiceReviews(queryDto);
return {
reviews,
@@ -2,11 +2,51 @@ import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { DanakServiceReviewQueryDto } from "../DTO/danak-service-review-query.dto";
import { DanakServiceReview } from "../entities/danak-service-review.entity";
import { ReviewStatus } from "../enums/review-status.enum";
@Injectable()
export class DanakServiceReviewRepository extends Repository<DanakServiceReview> {
constructor(@InjectRepository(DanakServiceReview) danakServiceReviewRepository: Repository<DanakServiceReview>) {
super(danakServiceReviewRepository.target, danakServiceReviewRepository.manager, danakServiceReviewRepository.queryRunner);
}
async getAverageRating(serviceId: string): Promise<number> {
const result = await this.createQueryBuilder("review")
.select("CAST(AVG(review.rating) AS DECIMAL(10,2))", "averageRating")
.where("review.serviceId = :serviceId", { serviceId })
.andWhere("review.status = :status", { status: ReviewStatus.APPROVED })
.getRawOne();
return result?.averageRating ? parseFloat(result.averageRating) : 0;
}
async getDanakServiceReviews(queryDto: DanakServiceReviewQueryDto) {
const { limit, skip } = PaginationUtils(queryDto);
const reviewsQueryBuilder = this.createQueryBuilder("review")
.leftJoinAndSelect("review.user", "user")
.leftJoinAndSelect("review.service", "service")
.select([
"review.id",
"review.title",
"review.comment",
"review.rating",
"review.status",
"review.createdAt",
"user.id",
"user.firstName",
"user.lastName",
"user.profilePic",
"service.id",
"service.name",
])
.orderBy("review.createdAt", "DESC")
.skip(skip)
.take(limit);
return reviewsQueryBuilder.getManyAndCount();
}
}
@@ -2,9 +2,11 @@ import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Brackets, IsNull, Not, Repository } from "typeorm";
import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum";
import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { DanakServicesQueryDto, DanakServicesSearchQueryDto } from "../DTO/danak-services-search-query.dto";
import { DanakServicesGlobalSearchDto, DanakServicesQueryDto, DanakServicesSearchQueryDto } from "../DTO/danak-services-search-query.dto";
import { DanakService } from "../entities/danak-service.entity";
import { ReviewStatus } from "../enums/review-status.enum";
@Injectable()
export class DanakServicesRepository extends Repository<DanakService> {
@@ -86,4 +88,89 @@ export class DanakServicesRepository extends Repository<DanakService> {
count,
};
}
//+********************
async getDanakServiceById(serviceId: string, isAdmin: boolean) {
const serviceQueryBuilder = this.createQueryBuilder("service")
.where("service.deletedAt IS NULL")
.leftJoin("service.category", "category")
.addSelect(["category.id", "category.title"])
.leftJoinAndSelect("service.images", "images")
.leftJoinAndSelect("service.subscriptionPlans", "subscriptionPlans")
.leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
.leftJoin("reviews.user", "user")
.addSelect([
"reviews.id",
"reviews.comment",
"reviews.rating",
"reviews.title",
"reviews.createdAt",
"reviews.status",
"user.id",
"user.firstName",
"user.lastName",
"user.profilePic",
])
// .loadRelationCountAndMap("service.reviewCount", "service.reviews")
// .addSelect(
// (subQuery) =>
// subQuery
// .select("CAST(AVG(rating) AS DECIMAL(10,2))", "averageRating")
// .from(DanakServiceReview, "r")
// .where("r.serviceId = service.id")
// .andWhere("r.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED }),
// "averageRating",
// )
.andWhere("service.id = :serviceId", { serviceId })
.groupBy("service.id,category.id, images.id, subscriptionPlans.id, reviews.id, user.id");
if (!isAdmin) {
serviceQueryBuilder
.andWhere("service.isActive = :isActive", { isActive: true })
.andWhere("subscriptionPlans.isActive = :planActive", { planActive: true });
}
serviceQueryBuilder.orderBy("subscriptionPlans.price", "ASC");
const danakService = await serviceQueryBuilder.getOne();
return danakService;
}
//+********************
async getUsersByService(serviceId: string) {
const users = await this.createQueryBuilder("service")
.innerJoin("service.subscriptionPlans", "subscriptionPlans")
.innerJoin("subscriptionPlans.userSubscriptions", "userSubscriptions")
.innerJoin("user", "user", "userSubscriptions.userId = user.id")
.select([
"DISTINCT user.id AS id",
"user.firstName AS firstName",
"user.lastName AS lastName",
"user.email AS email",
"user.profilePic AS profilePic",
])
.where("service.id = :serviceId", { serviceId })
.andWhere("userSubscriptions.status = :status", { status: SubscriptionStatus.ACTIVE })
.andWhere("userSubscriptions.endDate > NOW()")
.getRawMany();
return users;
}
async searchServices(queryDto: DanakServicesGlobalSearchDto) {
const servicesQuery = this.createQueryBuilder("service")
.leftJoin("service.category", "category")
.leftJoin("service.subscriptionPlans", "subscriptionPlans")
.select(["service.id", "service.name", "service.description", "service.icon", "category.id", "category.title"])
.where("service.isActive = :isActive", { isActive: true })
.andWhere("service.deletedAt IS NULL")
.andWhere("category.isActive = :categoryActive", { categoryActive: true })
.andWhere("category.deletedAt IS NULL")
.andWhere("subscriptionPlans.isActive = :planActive", { planActive: true });
if (queryDto.q) {
servicesQuery.andWhere("service.name ILIKE :searchQuery", { searchQuery: `%${queryDto.q}%` });
}
return servicesQuery.getMany();
}
}
+41 -33
View File
@@ -1,51 +1,59 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { ArrayUnique, IsArray, IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
IsBoolean,
IsDateString,
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
Max,
Min,
ValidateIf,
} from "class-validator";
import { DiscountMessage } from "../../../common/enums/message.enum";
import { DiscountCalculationType, DiscountType } from "../enums/discount-type.enum";
import { DiscountType } from "../enums/discount-type.enum";
export class CreateDiscountDto {
@ApiProperty({ description: "title of discount", type: String, example: "discount 1" })
@IsNotEmpty({ message: DiscountMessage.TITLE_REQUIRED })
@IsString({ message: DiscountMessage.TITLE_INVALID })
@IsString({ message: DiscountMessage.TITLE_STRING })
@ApiProperty({ description: "The title of the discount", example: "Summer Sale" })
title: string;
@ApiProperty({ description: "discount type", enum: DiscountType, example: DiscountType.CODE })
@IsNotEmpty({ message: DiscountMessage.TYPE_REQUIRED })
@IsEnum(DiscountType, { message: DiscountMessage.TYPE_INVALID })
@IsNotEmpty({ message: DiscountMessage.DISCOUNT_TYPE_REQUIRED })
@IsEnum(DiscountType, { message: DiscountMessage.INVALID_DISCOUNT_TYPE })
@ApiProperty({ description: "The type of discount", example: DiscountType.PERCENTAGE })
type: DiscountType;
@ApiProperty({ description: "calculation type", enum: DiscountCalculationType, example: DiscountCalculationType.FIXED })
@IsNotEmpty({ message: DiscountMessage.CALCULATION_TYPE_REQUIRED })
@IsEnum(DiscountCalculationType, { message: DiscountMessage.CALCULATION_TYPE_INVALID })
calculationType: DiscountCalculationType;
@IsNumber({}, { message: DiscountMessage.VALUE_MUST_BE_A_NUMBER })
@Min(0, { message: DiscountMessage.VALUE_MUST_BE_AT_LEAST_0 })
@Max(100, { message: DiscountMessage.VALUE_MUST_NOT_EXCEED_100 })
@Type(() => Number)
@ApiProperty({ description: "The value of the discount", example: 20 })
value: number;
@ApiProperty({ description: "amount of discount", type: Number, example: 100 })
@IsNotEmpty({ message: DiscountMessage.AMOUNT_REQUIRED })
@IsNumber({}, { message: DiscountMessage.AMOUNT_INVALID })
amount: number;
@ApiProperty({ description: "start discount date", type: String, format: "date-time", example: "2025-01-01T00:00:00Z" })
@IsNotEmpty({ message: DiscountMessage.START_DATE_REQUIRED })
@IsDateString({}, { message: DiscountMessage.START_DATE_INVALID })
@IsDateString({}, { message: DiscountMessage.START_DATE_MUST_BE_A_VALID_DATE_STRING })
@IsNotEmpty({ message: DiscountMessage.START_DATE_IS_REQUIRED })
@ApiProperty({ description: "The start date of the discount", example: "2023-06-01T00:00:00Z" })
startDate: string;
@ApiProperty({ description: "end discount date", type: String, format: "date-time", example: "2025-12-31T23:59:59Z" })
@IsNotEmpty({ message: DiscountMessage.END_DATE_REQUIRED })
@IsDateString({}, { message: DiscountMessage.END_DATE_INVALID })
@IsDateString({}, { message: DiscountMessage.END_DATE_MUST_BE_A_VALID_DATE_STRING })
@IsNotEmpty({ message: DiscountMessage.END_DATE_IS_REQUIRED })
@ApiProperty({ description: "The end date of the discount", example: "2023-06-30T23:59:59Z" })
endDate: string;
@ApiPropertyOptional({ description: "لیست آیدی‌های خدمات", type: [String], example: ["f47ac10b-58cc-4372-a567-0e02b2c3d479"] })
@IsBoolean({ message: DiscountMessage.IS_ACTIVE_MUST_BE_A_BOOLEAN })
@IsOptional()
@ArrayUnique({ message: DiscountMessage.SERVICES_DUPLICATE })
@IsArray({ message: DiscountMessage.SERVICES_INVALID })
@IsUUID("4", { each: true, message: DiscountMessage.SERVICES_UUID_INVALID })
subscriptionPlanIds?: string[];
@ApiProperty({ description: "Indicates if the discount is active", example: true })
isActive?: boolean;
@ApiPropertyOptional({ description: "لیست آیدی‌های کاربران", type: [String], example: ["f47ac10b-58cc-4372-a567-0e02b2c3d479"] })
@IsArray({ message: "Target products must be an array" })
@IsOptional()
@ArrayUnique({ message: DiscountMessage.USER_DUPLICATE })
@IsArray({ message: DiscountMessage.USER_INVALID })
@IsUUID("4", { each: true, message: DiscountMessage.USER_UUID_INVALID })
userIds?: string[];
@ValidateIf((o) => o.targetProducts !== null && o.targetProducts !== undefined)
@IsString({ each: true, message: "Each target product must be a string" })
@ApiProperty({ description: "List of target product IDs", example: ["prod-1", "prod-2"] })
targetProducts?: string[];
}
@@ -1,12 +0,0 @@
// import { ApiPropertyOptional } from "@nestjs/swagger";
// import { IsOptional, IsString } from "class-validator";
// import { PaginationDto } from "../../../common/DTO/pagination.dto";
// import { FinancialMessage } from "../../../common/enums/message.enum";
// export class SearchDiscountsDto extends PaginationDto {
// @IsOptional()
// @IsString({ message: FinancialMessage.SEARCH_QUERY_MUST_BE_A_STRING })
// @ApiPropertyOptional({ description: "search query", example: "search query" })
// q: string;
// }
@@ -0,0 +1,7 @@
import { IsBoolean, IsNotEmpty } from "class-validator";
export class ToggleActiveDto {
@IsBoolean()
@IsNotEmpty()
isActive: boolean;
}
@@ -0,0 +1,44 @@
import { Type } from "class-transformer";
import { IsArray, IsBoolean, IsDateString, IsEnum, IsNumber, IsOptional, IsString, Max, Min, ValidateIf } from "class-validator";
import { DiscountStatus } from "../enums/discount-status.enum";
import { DiscountType } from "../enums/discount-type.enum";
export class UpdateDiscountDto {
@IsString()
@IsOptional()
title?: string;
@IsEnum(DiscountType)
@IsOptional()
type?: DiscountType;
@IsEnum(DiscountStatus)
@IsOptional()
status?: DiscountStatus;
@IsNumber()
@IsOptional()
@Min(0)
@Max(100)
@Type(() => Number)
value?: number;
@IsDateString()
@IsOptional()
startDate?: string;
@IsDateString()
@IsOptional()
endDate?: string;
@IsBoolean()
@IsOptional()
isActive?: boolean;
@IsArray()
@IsOptional()
@ValidateIf((o) => o.targetProducts !== null && o.targetProducts !== undefined)
@IsString({ each: true })
targetProducts?: string[];
}
@@ -0,0 +1,11 @@
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
export class ValidateDiscountDto {
@IsString()
@IsNotEmpty()
code: string;
@IsString()
@IsOptional()
productId?: string;
}
+65 -46
View File
@@ -1,58 +1,77 @@
// import { Body, Controller, Get, Param, Patch, Post, Query } from "@nestjs/common";
// import { ApiOperation, ApiTags } from "@nestjs/swagger";
// import { Body, Controller, Post } from "@nestjs/common";
// import { ApiOperation, ApiResponse } from "@nestjs/swagger";
// import { CreateDiscountDto } from "./DTO/create-discount.dto";
// // import { SearchDiscountsDto } from "./DTO/discount-search-query.dto";
// import { DiscountService } from "./providers/discounts.service";
// import { Discount } from "./entities/discount.entity";
// import { DiscountsService } from "./providers/discounts.service";
// import { AdminRoute } from "../../common/decorators/admin.decorator";
// import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
// import { UserDec } from "../../common/decorators/user.decorator";
// import { ParamDto } from "../../common/DTO/param.dto";
// import { PermissionsDec } from "../../common/decorators/permission.decorator";
// import { PermissionEnum } from "../users/enums/permission.enum";
// @ApiTags("discounts")
// @AuthGuards()
// @Controller("discounts")
// export class DiscountController {
// constructor(private readonly discountService: DiscountService) {}
// //************************************ */
// @AuthGuards()
// export class DiscountsController {
// constructor(private readonly discountsService: DiscountsService) {}
// @ApiOperation({ summary: "Create a new discount" })
// @AdminRoute()
// @PermissionsDec(PermissionEnum.DISCOUNTS)
// @Post()
// async create(@Body() createDiscountDto: CreateDiscountDto) {
// return this.discountService.create(createDiscountDto);
// @ApiResponse({ status: 201, description: "Discount created successfully" })
// create(@Body() createDiscountDto: CreateDiscountDto): Promise<Discount> {
// return this.discountsService.create(createDiscountDto);
// }
// //************************************ */
// // @AdminRoute()
// // @PermissionsDec(PermissionEnum.DISCOUNTS)
// // @Get()
// // @ApiOperation({ summary: "Get all discounts" })
// // findAll(@Query() query: any) {}
// @ApiOperation({ summary: "Retrieve all discounts" })
// @Get()
// async findAll(@Query() queryDto: SearchDiscountsDto) {
// return this.discountService.findAll(queryDto);
// }
// //************************************ */
// @ApiOperation({ summary: "Retrieve a discount by ID" })
// @Get(":id")
// async findOne(@Param() paramDto: ParamDto) {
// return this.discountService.findOne(paramDto.id);
// }
// //************************************ */
// @ApiOperation({ summary: "Retrieve a discounts by userID" })
// @Get("/user-discounts")
// async findDiscountsByUsers(@Query() queryDto: SearchDiscountsDto, @UserDec("id") userId: string) {
// return this.discountService.findDiscountsByUser(queryDto, userId);
// }
// //************************************ */
// @ApiOperation({ summary: "Toggle active/deactive discount" })
// @Patch(":id/toggle")
// async toggleActive(@Param() paramDto: ParamDto) {
// return this.discountService.toggleActive(paramDto.id);
// }
// //************************************ */
// // @AdminRoute()
// // @PermissionsDec(PermissionEnum.DISCOUNTS)
// // @Get(":id")
// // @ApiOperation({ summary: "Get discount by ID" })
// // findOne(@Param("id") id: string): Promise<Discount> {
// // return this.discountsService.findById(id);
// // }
// // @AdminRoute()
// // @PermissionsDec(PermissionEnum.DISCOUNTS)
// // @Get(":id/products")
// // @ApiOperation({ summary: "Get products for a discount" })
// // getProducts(@Param("id") id: string): Promise<string[]> {
// // return this.discountsService.getProductsForDiscount(id);
// // }
// // @AdminRoute()
// // @PermissionsDec(PermissionEnum.DISCOUNTS)
// // @Put(":id")
// // @ApiOperation({ summary: "Update discount" })
// // update(@Param("id") id: string, @Body() updateDiscountDto: UpdateDiscountDto): Promise<Discount> {
// // return this.discountsService.update(id, updateDiscountDto);
// // }
// // @AdminRoute()
// // @PermissionsDec(PermissionEnum.DISCOUNTS)
// // @Patch(":id/toggle-active")
// // @ApiOperation({ summary: "Toggle discount active state" })
// // toggleActive(@Param("id") id: string, @Body() toggleActiveDto: ToggleActiveDto): Promise<Discount> {
// // return this.discountsService.toggleActive(id, toggleActiveDto.isActive);
// // }
// // @AdminRoute()
// // @PermissionsDec(PermissionEnum.DISCOUNTS)
// // @Delete(":id")
// // @ApiOperation({ summary: "Delete discount" })
// // remove(@Param("id") id: string): Promise<void> {
// // return this.discountsService.delete(id);
// // }
// // @Post("validate")
// // @ApiOperation({ summary: "Validate discount code" })
// // validateDiscount(@Body() validateDiscountDto: ValidateDiscountDto, @UserDec() user: User): Promise<Discount> {
// // return this.discountsService.validateDiscount(validateDiscountDto.code, user, validateDiscountDto.productId);
// // }
// }
+18 -14
View File
@@ -1,16 +1,20 @@
// import { Module } from "@nestjs/common";
// import { TypeOrmModule } from "@nestjs/typeorm";
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
// import { DiscountController } from "./discounts.controller";
// import { Discount } from "./entities/discount.entity";
// import { DiscountService } from "./providers/discounts.service";
// import { DiscountRepository } from "./repositories/discount.repository";
// import { SubscriptionsModule } from "../subscriptions/subscriptions.module";
// import { UsersModule } from "../users/users.module";
// import { DiscountsController } from "./discounts.controller";
import { UsersModule } from "../users/users.module";
import { DiscountService } from "./entities/discount-product.entity";
import { Discount } from "./entities/discount.entity";
import { UsageDiscount } from "./entities/usage-discount.entity";
// import { DiscountsService } from "./providers/discounts.service";
// import { DiscountServicesRepository } from "./repositories/discount-services.repository";
// import { DiscountRepository } from "./repositories/discounts.repository";
// import { UsageDiscountRepository } from "./repositories/usage-discounts.repository";
// @Module({
// imports: [TypeOrmModule.forFeature([Discount]), SubscriptionsModule, UsersModule],
// providers: [DiscountService, DiscountRepository],
// controllers: [DiscountController],
// })
// export class DiscountModule {}
@Module({
imports: [TypeOrmModule.forFeature([Discount, UsageDiscount, DiscountService]), UsersModule],
// controllers: [DiscountsController],
// providers: [DiscountsService, DiscountRepository, UsageDiscountRepository, DiscountServicesRepository],
// exports: [DiscountsService],
})
export class DiscountsModule {}
@@ -0,0 +1,14 @@
import { Entity, ManyToOne } from "typeorm";
import { Discount } from "./discount.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { DanakService } from "../../danak-services/entities/danak-service.entity";
@Entity()
export class DiscountService extends BaseEntity {
@ManyToOne(() => Discount, (discount) => discount.discountServices, { nullable: false, onDelete: "CASCADE" })
discount: Discount;
@ManyToOne(() => DanakService, (danakService) => danakService.discountServices, { nullable: false })
danakService: DanakService;
}
+16 -28
View File
@@ -1,34 +1,24 @@
// eslint-disable-next-line import/no-named-as-default
import Decimal from "decimal.js";
import { Column, Entity, JoinTable, ManyToMany, OneToMany } from "typeorm";
import { Column, Entity, OneToMany } from "typeorm";
import { DiscountService } from "./discount-product.entity";
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 { DiscountCalculationType, DiscountType } from "../enums/discount-type.enum";
import { DiscountType } from "../enums/discount-type.enum";
@Entity()
export class Discount extends BaseEntity {
@Column({ type: "varchar", length: 200, nullable: false })
@Column({ type: "varchar", length: 150 })
title: string;
@Column({ type: "enum", enum: DiscountType })
@Column({ type: "varchar", length: 100, unique: true })
code: string;
@Column({ type: "enum", enum: DiscountType, default: DiscountType.PERCENTAGE })
type: DiscountType;
@Column({ type: "enum", enum: DiscountCalculationType })
calculationType: DiscountCalculationType;
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
amount: Decimal;
@Column({ type: "boolean", default: true })
isActive: boolean;
@Column({ nullable: true })
code?: string;
@Column({ type: "decimal", precision: 16, scale: 2, transformer: new DecimalTransformer() })
value: number;
@Column({ type: "timestamptz" })
startDate: Date;
@@ -36,16 +26,14 @@ export class Discount extends BaseEntity {
@Column({ type: "timestamptz" })
endDate: Date;
@ManyToMany(() => SubscriptionPlan, { nullable: true })
@JoinTable()
subscriptionPlans: SubscriptionPlan[];
@Column({ type: "int", default: 0 })
usedCount: number;
@ManyToMany(() => User, (user) => user.discounts, { nullable: true })
@JoinTable()
users: User[];
@Column({ type: "boolean", default: true })
isActive: boolean;
@OneToMany(() => Invoice, (invoice) => invoice.discount)
invoice: Invoice;
@OneToMany(() => DiscountService, (discountService) => discountService.discount, { cascade: true })
discountServices: DiscountService[];
@OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount)
usages: UsageDiscount[];
+13 -8
View File
@@ -1,18 +1,23 @@
import { Column, Entity, JoinTable, ManyToMany, ManyToOne } from "typeorm";
// eslint-disable-next-line import/no-named-as-default
import Decimal from "decimal.js";
import { Column, Entity, ManyToOne } from "typeorm";
import { Discount } from "./discount.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
import { User } from "../../users/entities/user.entity";
@Entity()
export class UsageDiscount extends BaseEntity {
@Column({ type: "timestamptz" })
usageDate: Date;
@ManyToOne(() => Discount, (discount) => discount.usages)
@ManyToOne(() => Discount, (discount) => discount.usages, { nullable: false, onDelete: "CASCADE" })
discount: Discount;
@ManyToMany(() => User, (user) => user.usageDiscounts)
@JoinTable()
users: User[];
@ManyToOne(() => User, { nullable: false, onDelete: "CASCADE" })
user: User;
@Column({ type: "decimal", precision: 16, scale: 2, transformer: new DecimalTransformer() })
appliedAmount: Decimal;
@Column({ type: "timestamptz", nullable: false })
usedAt: Date;
}
@@ -0,0 +1,5 @@
export enum DiscountStatus {
ACTIVE = "active",
INACTIVE = "inactive",
EXPIRED = "expired",
}
+2 -7
View File
@@ -1,9 +1,4 @@
export enum DiscountType {
DIRECT = "DIRECT",
CODE = "CODE",
}
export enum DiscountCalculationType {
FIXED = "FIXED",
PERCENTAGE = "PERCENTAGE",
PERCENTAGE = 0,
FIXED_AMOUNT = 1,
}
+147 -135
View File
@@ -1,182 +1,194 @@
// import { randomBytes } from "node:crypto";
// import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
// import { In } from "typeorm";
// // eslint-disable-next-line import/no-named-as-default
// import Decimal from "decimal.js";
// import { CommonMessage, DiscountMessage, SubscriptionMessage, UserMessage } from "../../../common/enums/message.enum";
// import { SubscriptionsPlanRepository } from "../../subscriptions/repositories/subscriptions.repository";
// import { UserRepository } from "../../users/repositories/users.repository";
// import { PaginationUtils } from "../../utils/providers/pagination.utils";
// import { User } from "../../users/entities/user.entity";
// import { CreateDiscountDto } from "../DTO/create-discount.dto";
// import { SearchDiscountsDto } from "../DTO/discount-search-query.dto";
// import { DiscountRepository } from "../repositories/discount.repository";
// import { UpdateDiscountDto } from "../DTO/update-discount.dto";
// import { Discount } from "../entities/discount.entity";
// import { DiscountType } from "../enums/discount-type.enum";
// import { DiscountServicesRepository } from "../repositories/discount-services.repository";
// import { DiscountRepository } from "../repositories/discounts.repository";
// import { UsageDiscountRepository } from "../repositories/usage-discounts.repository";
// @Injectable()
// export class DiscountService {
// export class DiscountsService {
// constructor(
// private readonly discountRepository: DiscountRepository,
// private readonly subscriptionPlanRepository: SubscriptionsPlanRepository,
// private readonly userRepository: UserRepository,
// private readonly discountServicesRepository: DiscountServicesRepository,
// private readonly usageDiscountRepository: UsageDiscountRepository,
// ) {}
// /******************************************** */
// async create(createDiscountDto: CreateDiscountDto) {
// const { subscriptionPlanIds, ...discountData } = createDiscountDto;
// const existingDiscount = await this.discountRepository.findByCode(createDiscountDto.title);
// if (existingDiscount) {
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.ALREADY_EXISTS);
// }
// const code = await this.generateUniqueCouponCode();
// // Create the discount without product relations
// const { targetProducts, ...discountData } = createDiscountDto;
// const discount = this.discountRepository.create({
// const discount = await this.discountRepository.create({
// ...discountData,
// code,
// startDate: new Date(createDiscountDto.startDate),
// endDate: new Date(createDiscountDto.endDate),
// status: createDiscountDto.status || DiscountStatus.ACTIVE,
// isActive: createDiscountDto.isActive !== undefined ? createDiscountDto.isActive : true,
// });
// if (createDiscountDto.subscriptionPlanIds && createDiscountDto.subscriptionPlanIds.length) {
// const subscriptionPlans = await this.subscriptionPlanRepository.find({
// where: { id: In(createDiscountDto.subscriptionPlanIds) },
// });
// if (subscriptionPlans.length !== createDiscountDto.subscriptionPlanIds.length) {
// throw new NotFoundException(SubscriptionMessage.NOT_FOUND);
// // Add product relations if provided
// if (targetProducts && targetProducts.length > 0) {
// await this.discountServicesRepository.createMany(discount.id, targetProducts);
// }
// discount.subscriptionPlans = subscriptionPlans;
// // Fetch the complete discount with relations
// const fullDiscount = await this.discountRepository.findById(discount.id);
// if (!fullDiscount) {
// throw new NotFoundException(DISCOUNT_ERROR_MESSAGES.NOT_FOUND);
// }
// return fullDiscount;
// }
// if (createDiscountDto.userIds) {
// const users = await this.userRepository.find({ where: { id: In(createDiscountDto.userIds) } });
// if (users.length !== createDiscountDto.userIds.length) {
// throw new NotFoundException(UserMessage.USER_NOT_FOUND);
// async findAll(options?: any): Promise<[Discount[], number]> {
// return this.discountRepository.findAll(options);
// }
// discount.users = users;
// }
// await this.discountRepository.save(discount);
// return {
// message: CommonMessage.CREATED,
// discount,
// };
// }
// /******************************************** */
// async findByCode(code: string) {
// const discount = await this.discountRepository.findOne({
// where: { code, isActive: true },
// relations: ["subscriptionPlans"],
// });
// async findById(id: string): Promise<Discount> {
// const discount = await this.discountRepository.findById(id);
// if (!discount) {
// throw new NotFoundException(DiscountMessage.NOT_FOUND);
// throw new NotFoundException(DISCOUNT_ERROR_MESSAGES.NOT_FOUND);
// }
// return { discount };
// return discount;
// }
// /******************************************** */
// async update(id: string, updateDiscountDto: UpdateDiscountDto): Promise<Discount> {
// // Validate discount exists
// await this.findById(id);
// async findDiscountsByUser(queryDto: SearchDiscountsDto, userId: string) {
// const user = await this.userRepository.findOneBy({
// id: userId,
// });
// if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
// // Handle product relations separately
// const { targetProducts, startDate: startDateStr, endDate: endDateStr, ...otherFields } = updateDiscountDto;
// const { limit, skip } = PaginationUtils(queryDto);
// // Initialize update data with other fields
// const updateData: Partial<Discount> = { ...otherFields };
// const queryBuilder = this.discountRepository.createQueryBuilder("discount");
// queryBuilder
// .leftJoin("discount.users", "user")
// .where("user.id = :userId", { userId: user.id })
// .leftJoinAndSelect("discount.subscriptionPlans", "subscriptionPlans")
// .leftJoin("subscriptionPlans.service", "service")
// .addSelect(["service.id", "service.name", "service.title", "service.link", "service.icon", "service.description"]);
// if (queryDto.q) {
// queryBuilder.andWhere("discount.code ILIKE :q", { q: `%${queryDto.q}%` });
// // Add date fields if they exist
// if (startDateStr) {
// updateData.startDate = new Date(startDateStr);
// }
// queryBuilder.orderBy("discount.createdAt", "DESC").skip(skip).take(limit);
// const [discounts, count] = await queryBuilder.getManyAndCount();
// return {
// discounts,
// count,
// paginate: true,
// };
// if (endDateStr) {
// updateData.endDate = new Date(endDateStr);
// }
// /******************************************** */
// async findAll(queryDto: SearchDiscountsDto) {
// const { limit, skip } = PaginationUtils(queryDto);
// const queryBuilder = this.discountRepository.createQueryBuilder("discount");
// queryBuilder
// .leftJoinAndSelect("discount.subscriptionPlans", "subscriptionPlans")
// .leftJoin("subscriptionPlans.service", "service")
// .addSelect(["service.id", "service.name", "service.title", "service.link", "service.icon", "service.description"])
// .leftJoinAndSelect("discount.users", "users");
// if (queryDto.q) {
// queryBuilder.andWhere("discount.code ILIKE :q", { q: `%${queryDto.q}%` });
// // Update the discount entity
// const updatedDiscount = await this.discountRepository.update(id, updateData);
// if (!updatedDiscount) {
// throw new NotFoundException(DISCOUNT_ERROR_MESSAGES.NOT_FOUND);
// }
// queryBuilder.orderBy("discount.createdAt", "DESC").skip(skip).take(limit);
// // Handle product relations if provided
// if (targetProducts !== undefined) {
// // Remove existing relations
// await this.discountServicesRepository.deleteByDiscountId(id);
// const [discounts, count] = await queryBuilder.getManyAndCount();
// return {
// discounts,
// count,
// paginate: true,
// };
// // Add new relations if not empty
// if (targetProducts.length > 0) {
// await this.discountServicesRepository.createMany(id, targetProducts);
// }
// }
// /******************************************** */
// async findOne(id: string) {
// const discount = await this.discountRepository.findOne({
// where: { id },
// relations: ["subscriptionPlans"],
// });
// if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
// return { discount };
// // Return the updated discount with relations
// return this.findById(id);
// }
// /******************************************** */
// async delete(id: string): Promise<void> {
// const discount = await this.findById(id);
// await this.discountRepository.delete(discount.id);
// }
// async toggleActive(id: string, isActive: boolean): Promise<Discount> {
// await this.findById(id);
// const updatedDiscount = await this.discountRepository.update(id, { isActive });
// if (!updatedDiscount) {
// throw new NotFoundException(DISCOUNT_ERROR_MESSAGES.NOT_FOUND);
// }
// return updatedDiscount;
// }
// async validateDiscount(code: string, user: User, productId?: string): Promise<Discount> {
// const discount = await this.discountRepository.findValidByCode(code);
// async toggleActive(id: string) {
// const discount = await this.discountRepository.findOne({ where: { id } });
// if (!discount) {
// throw new BadRequestException(DiscountMessage.NOT_FOUND);
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.INVALID);
// }
// discount.isActive = !discount.isActive;
// const updatedDiscount = await this.discountRepository.save(discount);
// return {
// message: CommonMessage.UPDATE_SUCCESS,
// updatedDiscount,
// };
// // Check if user has already used this discount
// const usageCount = await this.usageDiscountRepository.countByUserAndDiscount(user.id, discount.id);
// if (usageCount > 0) {
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.ALREADY_USED);
// }
// /******************************************** */
// async generateUniqueCouponCode(): Promise<string> {
// const code = this.generateCouponCode();
// const exists = await this.discountRepository.findOne({ where: { code } });
// if (exists) return this.generateUniqueCouponCode();
// return code;
// }
// /******************************************** */
// private generateCouponCode(length = 8) {
// return randomBytes(length).toString("hex").slice(0, length).toUpperCase();
// // Check if product is valid for this discount
// if (productId && discount.discountServices && discount.discountServices.length > 0) {
// const hasProduct = await this.discountServicesRepository.hasProductForDiscount(discount.id, productId);
// if (!hasProduct) {
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.INVALID_PRODUCT);
// }
// }
// return discount;
// }
// async applyDiscount(amount: number, discountId: string, productId?: string): Promise<number> {
// const discount = await this.findById(discountId);
// if (!discount.isValid) {
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.INVALID);
// }
// // Check if product is valid for this discount
// if (productId && discount.discountServices && discount.discountServices.length > 0) {
// const hasProduct = await this.discountServicesRepository.hasProductForDiscount(discount.id, productId);
// if (!hasProduct) {
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.INVALID_PRODUCT);
// }
// }
// let discountAmount = 0;
// if (discount.type === DiscountType.PERCENTAGE) {
// discountAmount = (amount * discount.value) / 100;
// } else if (discount.type === DiscountType.FIXED_AMOUNT) {
// discountAmount = discount.value;
// }
// // Ensure discount doesn't exceed the original amount
// if (discountAmount > amount) {
// discountAmount = amount;
// }
// return discountAmount;
// }
// async recordDiscountUsage(userId: string, discountId: string, appliedAmount: number): Promise<void> {
// const discount = await this.findById(discountId);
// // Increment usage count
// await this.discountRepository.update(discount.id, {
// usedCount: discount.usedCount + 1,
// });
// // Record usage
// await this.usageDiscountRepository.create({
// user: { id: userId } as User,
// discount: { id: discountId } as Discount,
// appliedAmount: new Decimal(appliedAmount),
// usedAt: new Date(),
// });
// }
// async getProductsForDiscount(discountId: string): Promise<string[]> {
// const discountServices = await this.discountServicesRepository.findByDiscountId(discountId);
// return discountServices.map((service) => service.danakService.id);
// }
// }
@@ -0,0 +1,57 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { DiscountService } from "../entities/discount-product.entity";
@Injectable()
export class DiscountServicesRepository extends Repository<DiscountService> {
constructor(@InjectRepository(DiscountService) repository: Repository<DiscountService>) {
super(repository.target, repository.manager, repository.queryRunner);
}
async createMany(discountId: string, productIds: string[]): Promise<DiscountService[]> {
const discountServices = productIds.map((productId) =>
this.create({
discount: { id: discountId },
danakService: { id: productId },
}),
);
return this.save(discountServices);
}
async findByDiscountId(discountId: string): Promise<DiscountService[]> {
return this.find({
where: { discount: { id: discountId } },
});
}
async findByProductId(productId: string): Promise<DiscountService[]> {
return this.find({
where: { danakService: { id: productId } },
relations: ["discount"],
});
}
async deleteByDiscountId(discountId: string): Promise<void> {
await this.createQueryBuilder().delete().where("discountId = :discountId", { discountId }).execute();
}
async hasProductForDiscount(discountId: string, productId: string): Promise<boolean> {
const count = await this.count({
where: {
discount: { id: discountId },
danakService: { id: productId },
},
});
return count > 0;
}
async findDiscountIdsForProduct(productId: string): Promise<string[]> {
const discountServices = await this.find({
where: { danakService: { id: productId } },
select: ["discount"],
relations: ["discount"],
});
return discountServices.map((dp) => dp.discount.id);
}
}
@@ -1,12 +0,0 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Discount } from "../entities/discount.entity";
@Injectable()
export class DiscountRepository extends Repository<Discount> {
constructor(@InjectRepository(Discount) discountRepository: Repository<Discount>) {
super(discountRepository.target, discountRepository.manager, discountRepository.queryRunner);
}
}
@@ -0,0 +1,72 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Discount } from "../entities/discount.entity";
@Injectable()
export class DiscountRepository extends Repository<Discount> {
constructor(@InjectRepository(Discount) repository: Repository<Discount>) {
super(repository.target, repository.manager, repository.queryRunner);
}
async findById(id: string): Promise<Discount | null> {
return this.findOne({
where: { id },
relations: ["productRelations"],
});
}
async findByCode(code: string): Promise<Discount | null> {
return this.findOne({
where: { code },
relations: ["productRelations"],
});
}
async findActiveByCode(code: string): Promise<Discount | null> {
return this.findOne({
where: {
code,
isActive: true,
},
relations: ["productRelations"],
});
}
async findAll(options?: any): Promise<[Discount[], number]> {
const query = this.createQueryBuilder("discount").leftJoinAndSelect("discount.productRelations", "productRelations");
if (options?.status) {
query.andWhere("discount.status = :status", { status: options.status });
}
if (options?.isActive !== undefined) {
query.andWhere("discount.isActive = :isActive", { isActive: options.isActive });
}
if (options?.search) {
query.andWhere("(discount.title ILIKE :search OR discount.code ILIKE :search)", {
search: `%${options.search}%`,
});
}
if (options?.productId) {
query.andWhere("EXISTS (SELECT 1 FROM discount_product dp WHERE dp.discountId = discount.id AND dp.productId = :productId)", {
productId: options.productId,
});
}
if (options?.sort) {
const [field, order] = options.sort.split(",");
query.orderBy(`discount.${field}`, order.toUpperCase());
} else {
query.orderBy("discount.createdAt", "DESC");
}
return query
.skip(options?.skip || 0)
.take(options?.take || 10)
.getManyAndCount();
}
}
@@ -1,12 +0,0 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { UsageDiscount } from "../entities/usage-discount.entity";
@Injectable()
export class UsageDiscountRepository extends Repository<UsageDiscount> {
constructor(@InjectRepository(UsageDiscount) usageDiscountRepository: Repository<UsageDiscount>) {
super(usageDiscountRepository.target, usageDiscountRepository.manager, usageDiscountRepository.queryRunner);
}
}
@@ -0,0 +1,39 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { UsageDiscount } from "../entities/usage-discount.entity";
@Injectable()
export class UsageDiscountRepository extends Repository<UsageDiscount> {
constructor(@InjectRepository(UsageDiscount) repository: Repository<UsageDiscount>) {
super(repository.target, repository.manager, repository.queryRunner);
}
async findByUserAndDiscount(userId: string, discountId: string): Promise<UsageDiscount | null> {
return this.findOne({
where: {
user: { id: userId },
discount: { id: discountId },
},
relations: ["user", "discount"],
});
}
async countByDiscount(discountId: string): Promise<number> {
return this.count({
where: {
discount: { id: discountId },
},
});
}
async countByUserAndDiscount(userId: string, discountId: string): Promise<number> {
return this.count({
where: {
user: { id: userId },
discount: { id: discountId },
},
});
}
}
@@ -1,11 +0,0 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator";
import { DiscountMessage } from "../../../common/enums/message.enum";
export class ApplyDiscountDto {
@ApiProperty({ description: "Discount code" })
@IsString({ message: DiscountMessage.DISCOUNT_CODE_STRING })
@IsNotEmpty({ message: DiscountMessage.DISCOUNT_CODE_REQUIRED })
discountCode: string;
}
+2 -2
View File
@@ -10,8 +10,8 @@ import { InvoicesService } from "./providers/invoices.service";
import { InvoicesRepository } from "./repositories/invoices.repository";
import { Discount } from "../discounts/entities/discount.entity";
import { UsageDiscount } from "../discounts/entities/usage-discount.entity";
import { DiscountRepository } from "../discounts/repositories/discount.repository";
import { UsageDiscountRepository } from "../discounts/repositories/usage-discount.repository";
import { DiscountRepository } from "../discounts/repositories/discounts.repository";
import { UsageDiscountRepository } from "../discounts/repositories/usage-discounts.repository";
import { NotificationModule } from "../notifications/notifications.module";
import { UsersModule } from "../users/users.module";
import { UtilsModule } from "../utils/utils.module";
@@ -16,6 +16,7 @@ import { ParamDto } from "../../common/DTO/param.dto";
import { PermissionEnum } from "../users/enums/permission.enum";
@Controller("subscriptions")
@AuthGuards()
export class SubscriptionsController {
constructor(
private readonly subscriptionService: SubscriptionsService,
@@ -23,7 +24,6 @@ export class SubscriptionsController {
) {}
@ApiOperation({ summary: "Create a subscription plan" })
@AuthGuards()
@PermissionsDec(PermissionEnum.SERVICES)
@Post()
createSubscription(@Body() createDto: AddSubscriptionsToServiceDto) {
@@ -31,7 +31,6 @@ export class SubscriptionsController {
}
@ApiOperation({ summary: "get all subscription plans" })
@AuthGuards()
@PermissionsDec(PermissionEnum.SERVICES)
@Get("service/:serviceId")
getServiceSubscriptions(@Param() paramDto: ServiceIdParamDto, @Query() queryDto: ServiceSubsQueryDto) {
@@ -39,21 +38,18 @@ export class SubscriptionsController {
}
@ApiOperation({ summary: "get all user subscriptions ==> user route" })
@AuthGuards()
@Get("user")
getUserSubscriptions(@UserDec("id") userId: string, @Query() queryDto: SearchUserSubsQueryDto) {
return this.subscriptionService.getUserSubscriptions(userId, queryDto);
}
@ApiOperation({ summary: "get a user subscription by id ==> user route" })
@AuthGuards()
@Get("user/:id")
getUserSubscriptionById(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
return this.subscriptionService.getUserSubscriptionById(paramDto.id, userId);
}
@ApiOperation({ summary: "toggle status of service subs" })
@AuthGuards()
@PermissionsDec(PermissionEnum.SERVICES)
@Post("toggle-status/:id")
toggleStatusOfServiceSub(@Param() paramDto: ParamDto) {
@@ -61,7 +57,6 @@ export class SubscriptionsController {
}
@ApiOperation({ summary: "get a subscription plan by id" })
@AuthGuards()
@Get(":id")
@PermissionsDec(PermissionEnum.SERVICES)
getSubscription(@Param() paramDto: ParamDto) {
@@ -69,7 +64,6 @@ export class SubscriptionsController {
}
@ApiOperation({ summary: "update a subscription plan by id" })
@AuthGuards()
@Patch(":id")
//TODO:fix this if needed
@PermissionsDec(PermissionEnum.SERVICES)
@@ -78,28 +72,24 @@ export class SubscriptionsController {
}
@ApiOperation({ summary: "subscribe to a service ==> user route" })
@AuthGuards()
@Post(":serviceId/subscribe")
subscribe(@Param() paramDto: ServiceIdParamDto, @Body() subscribeDto: SubscribeServiceDto, @UserDec("id") userId: string) {
return this.subscriptionService.subscribeToPlan(paramDto.serviceId, subscribeDto, userId);
}
@ApiOperation({ summary: "add quick access a service ==> user route" })
@AuthGuards()
@Post(":id/add-quick-access")
addQuickAccess(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
return this.userQuickAccessService.addQuickAccess(userId, paramDto.id);
}
@ApiOperation({ summary: "get quick access a service ==> user route" })
@AuthGuards()
@Get("quick-access")
getQuickAccess(@UserDec("id") userId: string) {
return this.userQuickAccessService.getUserQuickAccess(userId);
}
@ApiOperation({ summary: "remove quick access a service ==> user route" })
@AuthGuards()
@Delete(":id/remove-quick-access")
removeQuickAccess(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
return this.userQuickAccessService.removeQuickAccess(userId, paramDto.id);
+2 -7
View File
@@ -12,8 +12,6 @@ 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 { UsageDiscount } from "../../discounts/entities/usage-discount.entity";
import { Invoice } from "../../invoices/entities/invoice.entity";
import { LearningProgress } from "../../learnings/entities/learning-progress.entity";
import { Notification } from "../../notifications/entities/notification.entity";
@@ -115,17 +113,14 @@ export class User extends BaseEntity {
@OneToMany(() => LearningProgress, (learningProgress) => learningProgress.learning)
learningProgress: LearningProgress[];
@ManyToMany(() => Discount, (discount) => discount.subscriptionPlans)
discounts: Discount[];
@OneToOne(() => RealUser, (realUser) => realUser.user, { cascade: true, nullable: true })
realUser: RealUser;
@OneToOne(() => LegalUser, (legalUser) => legalUser.user, { cascade: true, nullable: true })
legalUser: LegalUser;
@ManyToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.users)
usageDiscounts: UsageDiscount[];
// @ManyToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.users)
// usageDiscounts: UsageDiscount[];
@OneToMany(() => UserAnnouncement, (userAnnouncement) => userAnnouncement.user)
userAnnouncements: UserAnnouncement[];