chore: update referral logic

This commit is contained in:
mahyargdz
2025-04-15 15:50:31 +03:30
parent bd4d9dbf0d
commit fb8f16deb4
19 changed files with 234 additions and 262 deletions
+6 -3
View File
@@ -348,6 +348,9 @@ export const enum ContactUsMessage {
TITLE_IS_REQUIRED = "عنوان مورد نیاز است",
TITLE_STRING = "عنوان باید یک رشته باشد",
NOT_FOUND = "پیامی با این ایدی یافت نشد",
BUSINESS_NAME_IS_REQUIRED = "نام شرکت مورد نیاز است",
BUSINESS_NAME_STRING = "نام شرکت باید یک رشته باشد",
BUSINESS_NAME_LENGTH = "نام شرکت باید بین ۲ تا ۱۰۰ کاراکتر باشد",
}
export const enum NotificationMessage {
@@ -630,7 +633,7 @@ export const enum BlogMessage {
TITLE_STRING = "عنوان باید یک رشته باشد",
DESCRIPTION_REQUIRED = "توضیحات الزامی است",
DESCRIPTION_STRING = "توضیحات باید یک رشته باشد",
TITLE_MAX_LENGTH = "حداکثر طول عنوان باید ۱۰۰ کاراکتر باشد",
TITLE_MAX_LENGTH = "حداکثر طول عنوان باید ۲۵۵ کاراکتر باشد",
DESCRIPTION_MAX_LENGTH = "حداکثر طول توضیحات باید ۲۵۵ کاراکتر باشد",
ICON_URL_REQUIRED = "آدرس آیکون الزامی است",
ICON_URL_INVALID = "آدرس آیکون نامعتبر است",
@@ -643,7 +646,7 @@ export const enum BlogMessage {
BLOG_NOT_FOUND = "بلاگ مورد نظر یافت نشد",
PREVIEW_CONTENT_REQUIRED = "محتوای پیشنمایش الزامی است",
PREVIEW_CONTENT_STRING = "محتوای پیشنمایش باید یک رشته باشد",
PREVIEW_CONTENT_MAX_LENGTH = "حداکثر طول محتوای پیشنمایش باید ۱۵۰ کاراکتر باشد",
PREVIEW_CONTENT_MAX_LENGTH = "حداکثر طول محتوای پیشنمایش باید 600 کاراکتر باشد",
CONTENT_REQUIRED = "محتوای بلاگ الزامی است",
CONTENT_STRING = "محتوای بلاگ باید یک رشته باشد",
CONTENT_MAX_LENGTH = "حداکثر طول محتوای بلاگ باید ۶۰۰ کاراکتر باشد",
@@ -674,7 +677,7 @@ export const enum BlogMessage {
SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد",
IS_PINNED_SHOULD_BE_A_BOOLEAN = "وضعیت سنجاق بودن باید یک بولین باشد",
SLUG_REQUIRED = "اسلاگ بلاگ الزامی است",
SLUG_MAX_LENGTH = "حداکثر طول اسلاگ بلاگ باید ۱۵۰ کاراکتر باشد",
SLUG_MAX_LENGTH = "حداکثر طول اسلاگ بلاگ باید ۲۵۵ کاراکتر باشد",
}
export const enum SliderMessage {
+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 : false,
synchronize: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
logging: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
migrationsTableName: "typeorm_migrations",
migrationsRun: false,
+1 -6
View File
@@ -67,12 +67,7 @@ export class AuthService {
const hashedPassword = await this.passwordService.hashPassword(completeRegistrationDto.password);
const user = await this.usersService.createUser(completeRegistrationDto, hashedPassword, queryRunner);
if (referralCode) {
await this.referralsService.useReferralCode({
referralCode,
userId: user.id,
});
}
if (referralCode) await this.referralsService.useReferralCode({ referralCode, userId: user.id }, queryRunner);
const tokens = await this.tokensService.generateTokens(user, queryRunner);
+4 -5
View File
@@ -6,25 +6,24 @@ import { BlogMessage } from "../../../common/enums/message.enum";
export class CreateBlogDto {
@IsNotEmpty({ message: BlogMessage.TITLE_REQUIRED })
@IsString({ message: BlogMessage.TITLE_STRING })
@MaxLength(150, { message: BlogMessage.TITLE_MAX_LENGTH })
@MaxLength(250, { message: BlogMessage.TITLE_MAX_LENGTH })
@ApiProperty({ description: "Title of the blog", example: "Blog Title" })
title: string;
@IsNotEmpty({ message: BlogMessage.SLUG_REQUIRED })
@IsString({ message: BlogMessage.SLUG_STRING })
@MaxLength(150, { message: BlogMessage.SLUG_MAX_LENGTH })
@MaxLength(250, { message: BlogMessage.SLUG_MAX_LENGTH })
@ApiProperty({ description: "Slug of the blog", example: "blog-title" })
slug: string;
@IsNotEmpty({ message: BlogMessage.PREVIEW_CONTENT_REQUIRED })
@IsString({ message: BlogMessage.PREVIEW_CONTENT_STRING })
@MaxLength(150, { message: BlogMessage.PREVIEW_CONTENT_MAX_LENGTH })
@MaxLength(600, { message: BlogMessage.PREVIEW_CONTENT_MAX_LENGTH })
@ApiProperty({ description: "Preview content of the blog", example: "Preview content of the blog" })
previewContent: string;
@IsNotEmpty({ message: BlogMessage.CONTENT_REQUIRED })
@IsString({ message: BlogMessage.CONTENT_STRING })
@MaxLength(600, { message: BlogMessage.CONTENT_MAX_LENGTH })
@ApiProperty({ description: "Content of the blog", example: "Content of the blog" })
content: string;
@@ -36,7 +35,7 @@ export class CreateBlogDto {
@IsNotEmpty({ message: BlogMessage.META_TITLE_REQUIRED })
@IsString({ message: BlogMessage.META_TITLE_STRING })
@MaxLength(60, { message: BlogMessage.META_TITLE_MAX_LENGTH })
@MaxLength(250, { message: BlogMessage.META_TITLE_MAX_LENGTH })
@ApiProperty({ description: "Meta title of the blog", example: "Meta title of the blog" })
metaTitle: string;
@@ -20,6 +20,6 @@ export class BlogCategory extends BaseEntity {
@OneToMany(() => Blog, (blog) => blog.category)
blogs: Blog[];
@DeleteDateColumn({ type: "timestamptz" })
@DeleteDateColumn()
deletedAt?: Date;
}
@@ -19,6 +19,6 @@ export class BlogComment extends BaseEntity {
@ManyToOne(() => User, (user) => user.blogComments)
user: User;
@ManyToOne(() => Blog, (blog) => blog.comments)
@ManyToOne(() => Blog, (blog) => blog.comments, { onDelete: "CASCADE" })
blog: Blog;
}
+8 -8
View File
@@ -7,10 +7,10 @@ import { User } from "../../users/entities/user.entity";
@Entity()
export class Blog extends BaseEntity {
@Column({ type: "varchar", length: 150, unique: true })
@Column({ type: "varchar", length: 250, unique: true })
title: string;
@Column({ type: "varchar", length: 150, unique: true })
@Column({ type: "varchar", length: 250, unique: true })
slug: string;
@Column({ type: "text" })
@@ -19,13 +19,13 @@ export class Blog extends BaseEntity {
@Column({ type: "text" })
content: string;
@Column({ type: "varchar", length: 150, nullable: true })
@Column({ type: "varchar", length: 250, nullable: true })
imageUrl: string;
@Column({ type: "varchar", length: 100, nullable: true })
@Column({ type: "varchar", length: 250, nullable: true })
metaTitle: string;
@Column({ type: "varchar", length: 160, nullable: true })
@Column({ type: "varchar", length: 250, nullable: true })
metaDescription: string;
@Column({ type: "simple-array", nullable: true })
@@ -40,15 +40,15 @@ export class Blog extends BaseEntity {
@Column({ type: "boolean", default: false })
isPinned: boolean;
@ManyToOne(() => User, (user) => user.blogs)
@ManyToOne(() => User, (user) => user.blogs, { onDelete: "CASCADE", nullable: false })
author: User;
@ManyToOne(() => BlogCategory, (category) => category.blogs)
@ManyToOne(() => BlogCategory, (category) => category.blogs, { nullable: false })
category: BlogCategory;
@OneToMany(() => BlogComment, (comment) => comment.blog)
comments: BlogComment[];
@DeleteDateColumn({ type: "timestamptz" })
@DeleteDateColumn()
deletedAt?: Date;
}
+27 -16
View File
@@ -56,9 +56,13 @@ export class BlogsService {
//*********************************** */
async deleteCategory(id: string) {
await this.findCategoryById(id);
const category = await this.blogCategoriesRepository.findOneBy({ id });
if (!category) throw new BadRequestException(BlogMessage.CATEGORY_NOT_FOUND);
await this.blogCategoriesRepository.softDelete(id);
await this.blogCategoriesRepository.softDelete({ id });
return {
message: CommonMessage.DELETED,
};
}
//*********************************** */
@@ -70,7 +74,10 @@ export class BlogsService {
await this.blogCategoriesRepository.save(category);
//
return { message: CommonMessage.UPDATE_SUCCESS, category };
return {
message: CommonMessage.UPDATE_SUCCESS,
category,
};
}
//*********************************** */
@@ -146,21 +153,24 @@ export class BlogsService {
//*********************************** */
async deleteBlog(id: string) {
await this.findBlogById(id);
const blog = await this.blogsRepository.findOneBy({ id });
if (!blog) throw new BadRequestException(BlogMessage.BLOG_NOT_FOUND);
await this.blogsRepository.softDelete(id);
return { message: CommonMessage.DELETED };
await this.blogsRepository.softDelete({ id });
return {
message: CommonMessage.DELETED,
};
}
//*********************************** */
async getBlogById(id: string, isAdmin: boolean = false) {
const blog = await this.findBlogById(id);
const comments = await this.blogCommentsRepository.findBlogCommentsByBlogId(id);
if (!isAdmin) {
blog.viewCount += 1;
await this.blogsRepository.save(blog);
}
if (isAdmin) return { blog };
blog.viewCount += 1;
await this.blogsRepository.save(blog);
const comments = await this.blogCommentsRepository.findBlogCommentsByBlogId(id);
return { blog, comments };
}
@@ -170,12 +180,13 @@ export class BlogsService {
const blog = await this.blogsRepository.findOneBySlug(slug);
if (!blog) throw new BadRequestException(BlogMessage.BLOG_NOT_FOUND);
if (!isAdmin) {
blog.viewCount += 1;
await this.blogsRepository.save(blog);
}
if (!isAdmin) return { blog };
return { blog };
blog.viewCount += 1;
await this.blogsRepository.save(blog);
const comments = await this.blogCommentsRepository.findBlogCommentsByBlogId(blog.id);
return { blog, comments };
}
//*********************************** */
@@ -13,73 +13,66 @@ export class BlogsRepository extends Repository<Blog> {
}
async findOneById(id: string): Promise<Blog | null> {
return this.findOne({
where: { id, deletedAt: IsNull() },
relations: {
author: true,
category: true,
},
select: {
id: true,
title: true,
previewContent: true,
metaTitle: true,
metaDescription: true,
imageUrl: true,
slug: true,
createdAt: true,
content: true,
tags: true,
viewCount: true,
isActive: true,
isPinned: true,
category: {
id: true,
title: true,
iconUrl: true,
description: true,
},
author: {
id: true,
firstName: true,
lastName: true,
},
},
});
const queryBuilder = this.createQueryBuilder("blog")
.leftJoinAndSelect("blog.author", "author")
.leftJoinAndSelect("blog.category", "category")
.where("blog.id = :id", { id })
.andWhere("blog.deletedAt IS NULL")
.select([
"blog.id",
"blog.title",
"blog.previewContent",
"blog.metaTitle",
"blog.metaDescription",
"blog.imageUrl",
"blog.slug",
"blog.createdAt",
"blog.content",
"blog.tags",
"blog.viewCount",
"blog.isActive",
"blog.isPinned",
"category.id",
"category.title",
"category.iconUrl",
"category.description",
"author.id",
"author.firstName",
"author.lastName",
]);
return queryBuilder.getOne();
}
async findOneBySlug(slug: string): Promise<Blog | null> {
return this.findOne({
where: { slug, deletedAt: IsNull(), isActive: true },
relations: {
author: true,
category: true,
},
select: {
id: true,
title: true,
previewContent: true,
metaTitle: true,
metaDescription: true,
imageUrl: true,
slug: true,
createdAt: true,
content: true,
tags: true,
viewCount: true,
category: {
id: true,
title: true,
iconUrl: true,
description: true,
},
author: {
id: true,
firstName: true,
lastName: true,
},
},
});
const queryBuilder = this.createQueryBuilder("blog")
.leftJoinAndSelect("blog.author", "author")
.leftJoinAndSelect("blog.category", "category")
.where("blog.slug = :slug", { slug })
.andWhere("blog.deletedAt IS NULL")
.andWhere("blog.isActive = :isActive", { isActive: true })
.select([
"blog.id",
"blog.title",
"blog.previewContent",
"blog.metaTitle",
"blog.metaDescription",
"blog.imageUrl",
"blog.slug",
"blog.createdAt",
"blog.content",
"blog.tags",
"blog.viewCount",
"category.id",
"category.title",
"category.iconUrl",
"category.description",
"author.id",
"author.firstName",
"author.lastName",
]);
return queryBuilder.getOne();
}
async fineOneByTitle(title: string): Promise<Blog | null> {
@@ -1,26 +1,33 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
import { IsEmail, IsMobilePhone, IsNotEmpty, IsString, Length } from "class-validator";
import { ContactUsMessage } from "../../../common/enums/message.enum";
import { AuthMessage, ContactUsMessage } from "../../../common/enums/message.enum";
export class CreateContactUsDto {
@IsNotEmpty({ message: ContactUsMessage.FULLNAME_IS_REQUIRED })
@IsString({ message: ContactUsMessage.FULLNAME_STRING })
@ApiProperty({ description: "full name of person need to create contact us", example: "متین جمشیدی" })
@ApiProperty({ description: "Full name of the person creating the contact request", example: "mahyar gdz" })
fullName: string;
@IsNotEmpty({ message: ContactUsMessage.FULLNAME_STRING })
@IsEmail()
@ApiProperty({ description: "email of person need to contact us", example: "matin@gmail.com" })
@IsNotEmpty({ message: AuthMessage.EMAIL_NOT_EMPTY })
@IsEmail({}, { message: AuthMessage.INVALID_EMAIL_FORMAT })
@ApiProperty({ description: "Email address", example: "ma@gmail.com" })
email: string;
@IsNotEmpty({ message: ContactUsMessage.TITLE_IS_REQUIRED })
@IsString({ message: ContactUsMessage.TITLE_STRING })
@ApiProperty({ description: "title of contact us", example: "عنوان پیام" })
title: string;
@IsNotEmpty({ message: AuthMessage.PHONE_NOT_EMPTY })
@IsMobilePhone("fa-IR", {}, { message: AuthMessage.INVALID_PHONE_FORMAT })
@Length(11, 11, { message: AuthMessage.PHONE_SHOULD_BE_11_DIGIT })
@ApiProperty({ description: "Phone number", default: "09922320740" })
phone: string;
@IsNotEmpty({ message: ContactUsMessage.BUSINESS_NAME_IS_REQUIRED })
@IsString({ message: ContactUsMessage.BUSINESS_NAME_STRING })
@Length(2, 100, { message: ContactUsMessage.BUSINESS_NAME_LENGTH })
@ApiProperty({ description: "Business name", example: "Tech Corp" })
businessName: string;
@IsNotEmpty({ message: ContactUsMessage.CONTENT_IS_REQUIRED })
@IsString({ message: ContactUsMessage.CONTENT_STRING })
@ApiProperty({ description: "content of contact us", example: "متن پیام" })
@ApiProperty({ description: "Content of the contact request", example: "متن پیام" })
content: string;
}
@@ -1,20 +1,23 @@
import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { ApiOperation } from "@nestjs/swagger";
import { CreateContactUsDto } from "./DTO/create-contact-us.dto";
import { SearchContactUsQueryDto } from "./DTO/search-contact-us-query.dto";
import { ContactUsService } from "./providers/contact-us.service";
import { AdminRoute } from "../../common/decorators/admin.decorator";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { PermissionsDec } from "../../common/decorators/permission.decorator";
import { SkipAuth } from "../../common/decorators/skip-auth.decorator";
import { ParamDto } from "../../common/DTO/param.dto";
import { PermissionEnum } from "../users/enums/permission.enum";
@Controller("contact-us")
@ApiTags("ContactUs")
@AuthGuards()
export class ContactUsController {
constructor(private readonly contactUsService: ContactUsService) {}
@ApiOperation({ summary: "Create a new contact us" })
@SkipAuth()
@Post()
createContactUs(@Body() createContactUsDto: CreateContactUsDto) {
return this.contactUsService.createContactUs(createContactUsDto);
@@ -22,9 +25,9 @@ export class ContactUsController {
//******************** */
@AuthGuards()
@PermissionsDec(PermissionEnum.CONTACTS_US)
@ApiOperation({ summary: "Get all contact us ===> login as admin" })
@AdminRoute()
@ApiOperation({ summary: "Get all contact us (admin route)" })
@Get()
getAllContactUs(@Query() queryDto: SearchContactUsQueryDto) {
return this.contactUsService.getAllContactUs(queryDto);
@@ -32,9 +35,9 @@ export class ContactUsController {
//******************** */
@AuthGuards()
@AdminRoute()
@PermissionsDec(PermissionEnum.CONTACTS_US)
@ApiOperation({ summary: "Get one contact us with id ===> login as admin" })
@ApiOperation({ summary: "Get one contact us with id (admin route)" })
@Get(":id")
getOneContactUs(@Param() paramDto: ParamDto) {
return this.contactUsService.getOneContactUs(paramDto.id);
@@ -7,18 +7,18 @@ export class ContactUs extends BaseEntity {
@Column({ type: "int", generated: "identity", insert: false })
numericId: number;
@Column({ type: "varchar", length: 150, nullable: false })
title: string;
@Column({ type: "varchar", length: 150 })
businessName: string;
@Column({ type: "varchar", length: 350 })
@Column({ type: "varchar", length: 250 })
fullName: string;
@Column({ type: "varchar", length: 11 })
phone: string;
@Column({ type: "varchar", length: 150 })
email: string;
@Column({ type: "text", nullable: false })
content: string;
@Column({ type: "boolean", default: false })
isRead: boolean;
}
@@ -13,9 +13,7 @@ export class ContactUsService {
//****************************** */
async createContactUs(createContactUsDto: CreateContactUsDto) {
const contactUs = this.contactUsRepository.create({
...createContactUsDto,
});
const contactUs = this.contactUsRepository.create({ ...createContactUsDto });
await this.contactUsRepository.save(contactUs);
return {
@@ -56,15 +54,10 @@ export class ContactUsService {
//****************************** */
async getOneContactUs(id: string) {
const contactUs = await this.contactUsRepository.findOneBy({
id,
});
const contactUs = await this.contactUsRepository.findOneBy({ id });
if (!contactUs) throw new BadRequestException(ContactUsMessage.NOT_FOUND);
contactUs.isRead = true;
await this.contactUsRepository.save(contactUs);
return { contactUs };
}
}
@@ -129,7 +129,6 @@ export class DanakServicesController {
}
@ApiOperation({ summary: "get services for user side" })
@AuthGuards()
@Get("suggested")
getDanakSuggestServices() {
return this.danakServicesService.getDanakSuggestServices();
@@ -2,6 +2,7 @@ import { InjectQueue } from "@nestjs/bullmq";
import { BadRequestException, HttpException, HttpStatus, Injectable, InternalServerErrorException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Queue } from "bullmq";
import dayjs from "dayjs";
// eslint-disable-next-line import/no-named-as-default
import Decimal from "decimal.js";
import { FastifyReply } from "fastify";
@@ -10,11 +11,9 @@ import { DataSource, Not, QueryRunner } from "typeorm";
import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { CommonMessage, PaymentMessage, UserMessage, WalletMessage } from "../../../common/enums/message.enum";
import { NotificationsService } from "../../notifications/providers/notifications.service";
import { ReferralReward } from "../../referrals/entities/referral-reward.entity";
import { Referral } from "../../referrals/entities/referral.entity";
import { ReferralRewardStatus } from "../../referrals/enums/referral-reward-status.enum";
import { ReferralStatus } from "../../referrals/enums/referral-status.enum";
// import { ReferralsService } from "../../referrals/providers/referrals.service";
import { ReferralsService } from "../../referrals/providers/referrals.service";
import { User } from "../../users/entities/user.entity";
import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { WalletsService } from "../../wallets/providers/wallets.service";
@@ -51,8 +50,8 @@ export class PaymentsService {
private readonly bankAccountsRepository: BankAccountsRepository,
private readonly walletsService: WalletsService,
private readonly depositRequestsRepository: DepositRequestsRepository,
private readonly referralsService: ReferralsService,
private dataSource: DataSource,
// private readonly referralsService: ReferralsService,
) {}
//*********************************** */
@@ -261,55 +260,38 @@ export class PaymentsService {
}
//*********************************** */
private async isFirstPurchase(userId: string, queryRunner: QueryRunner): Promise<boolean> {
private async isFirstPurchase(userId: string, queryRunner: QueryRunner) {
const paymentCount = await queryRunner.manager.count(Payment, {
where: { user: { id: userId }, status: PaymentStatus.COMPLETED },
});
return paymentCount === 0;
}
//*********************************** */
private async getPendingReferral(userId: string, queryRunner: QueryRunner): Promise<Referral | null> {
return queryRunner.manager.findOne(Referral, {
where: {
referredUser: { id: userId },
status: ReferralStatus.PENDING,
},
relations: { referrer: true },
});
}
//*********************************** */
private async handleReferralReward(payment: Payment, queryRunner: QueryRunner) {
const isFirstPurchase = await this.isFirstPurchase(payment.user.id, queryRunner);
if (!isFirstPurchase) return;
const referral = await this.getPendingReferral(payment.user.id, queryRunner);
const referral = await this.referralsService.getPendingReferral(payment.user.id, queryRunner);
if (!referral) return;
// Reward amount is 10% of the purchase amount
// 10%
const rewardAmount = new Decimal(payment.amount).mul(0.1);
// Create reward record
const reward = queryRunner.manager.create(ReferralReward, {
user: { id: referral.referrer.id },
referral: { id: referral.id },
amount: rewardAmount,
status: ReferralRewardStatus.PENDING,
});
await queryRunner.manager.save(reward);
const reward = await this.referralsService.createReferralReward(referral.referrer.id, referral.id, rewardAmount, queryRunner);
// Charge referrer's wallet
await this.walletsService.createReferralRewardTransaction(referral.referrer.id, rewardAmount, queryRunner);
// Update reward status
reward.status = ReferralRewardStatus.PAID;
reward.paidAt = new Date();
reward.paidAt = dayjs().toDate();
await queryRunner.manager.save(reward);
// Update referral status
referral.status = ReferralStatus.COMPLETED;
referral.completedAt = new Date();
referral.completedAt = dayjs().toDate();
await queryRunner.manager.save(referral);
}
//*********************************** */
@@ -16,8 +16,8 @@ export class ReferralReward extends BaseEntity {
@ManyToOne(() => Referral, (referral) => referral.rewards)
referral: Referral;
@Column({ type: "decimal", precision: 10, scale: 2, nullable: false, transformer: new DecimalTransformer() })
amount: Decimal;
@Column({ type: "decimal", precision: 10, scale: 2, nullable: true, transformer: new DecimalTransformer() })
amount?: Decimal;
@Column({ type: "enum", enum: ReferralRewardStatus, default: ReferralRewardStatus.PENDING })
status: ReferralRewardStatus;
@@ -3,13 +3,11 @@ import { randomBytes } from "node:crypto";
import { BadRequestException, Injectable } from "@nestjs/common";
// eslint-disable-next-line import/no-named-as-default
import Decimal from "decimal.js";
import { DataSource, QueryRunner } from "typeorm";
import { QueryRunner } from "typeorm";
import { ReferralMessage, UserMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity";
import { WalletsService } from "../../wallets/providers/wallets.service";
import { UseReferralCodeDto } from "../DTO/use-referral-code.dto";
import { ReferralRewardStatus } from "../enums/referral-reward-status.enum";
import { ReferralStatus } from "../enums/referral-status.enum";
import { ReferralCodesRepository } from "../repositories/referral-codes.repository";
import { ReferralRewardsRepository } from "../repositories/referral-rewards.repository";
@@ -20,8 +18,6 @@ export class ReferralsService {
private readonly referralsRepository: ReferralsRepository,
private readonly referralCodesRepository: ReferralCodesRepository,
private readonly referralRewardsRepository: ReferralRewardsRepository,
private readonly walletsService: WalletsService,
private readonly dataSource: DataSource,
) {}
async generateReferralCode(userId: string, queryRunner: QueryRunner) {
@@ -54,75 +50,76 @@ export class ReferralsService {
}
//*********************************** */
async useReferralCode(dto: UseReferralCodeDto) {
const queryRunner = this.dataSource.createQueryRunner();
async useReferralCode(dto: UseReferralCodeDto, queryRunner: QueryRunner) {
const referredUser = await queryRunner.manager.findOne(User, { where: { id: dto.userId } });
if (!referredUser) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const referralCode = await queryRunner.manager.findOne(this.referralCodesRepository.target, {
where: { code: dto.referralCode, isActive: true },
relations: { user: true },
});
if (!referralCode || !referralCode.user) throw new BadRequestException(ReferralMessage.INVALID_OR_INACTIVE_REFERRAL_CODE);
const referredUser = await queryRunner.manager.findOne(User, { where: { id: dto.userId } });
if (!referredUser) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
const existingReferral = await queryRunner.manager.findOne(this.referralsRepository.target, {
where: { referredUser: { id: referredUser.id } },
});
if (existingReferral) throw new BadRequestException(ReferralMessage.USER_ALREADY_REFERRED);
const referralCode = await queryRunner.manager.findOne(this.referralCodesRepository.target, {
where: { code: dto.referralCode, isActive: true },
relations: { user: true },
});
if (!referralCode || !referralCode.user) throw new BadRequestException(ReferralMessage.INVALID_OR_INACTIVE_REFERRAL_CODE);
const referral = queryRunner.manager.create(this.referralsRepository.target, {
referrer: { id: referralCode.user.id },
referredUser: { id: referredUser.id },
referralCode: { id: referralCode.id },
});
// Check if user has already been referred
const existingReferral = await queryRunner.manager.findOne(this.referralsRepository.target, {
where: { referredUser: { id: referredUser.id } },
});
if (existingReferral) throw new BadRequestException(ReferralMessage.USER_ALREADY_REFERRED);
await queryRunner.manager.save(referral);
// Create referral record
const referral = queryRunner.manager.create(this.referralsRepository.target, {
referrer: { id: referralCode.user.id },
referredUser: { id: referredUser.id },
referralCode: { id: referralCode.id },
status: ReferralStatus.PENDING,
});
referralCode.usedCount += 1;
await queryRunner.manager.save(referralCode);
await queryRunner.manager.save(referral);
// const reward = queryRunner.manager.create(this.referralRewardsRepository.target, {
// user: { id: referralCode.user.id },
// referral: { id: referral.id },
// });
// Increment usedCount
referralCode.usedCount += 1;
await queryRunner.manager.save(referralCode);
// await queryRunner.manager.save(reward);
// Create pending reward
const reward = queryRunner.manager.create(this.referralRewardsRepository.target, {
user: { id: referralCode.user.id },
referral: { id: referral.id },
amount: 100, // Set your reward amount here
status: ReferralRewardStatus.PENDING,
});
await queryRunner.manager.save(reward);
// Charge the referred user's wallet
const chargeAmount = new Decimal(100);
await this.walletsService.createReferralRewardTransaction(referredUser.id, chargeAmount, queryRunner);
// Update referral status to COMPLETED
referral.status = ReferralStatus.COMPLETED;
await queryRunner.manager.save(referral);
// Update reward status to PAID
reward.status = ReferralRewardStatus.PAID;
reward.paidAt = new Date();
await queryRunner.manager.save(reward);
await queryRunner.commitTransaction();
return { referral, reward };
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
return referral;
}
//*********************************** */
async getPendingReferral(userId: string, queryRunner: QueryRunner) {
return queryRunner.manager.findOne(this.referralsRepository.target, {
where: {
referredUser: { id: userId },
status: ReferralStatus.PENDING,
},
relations: { referrer: true },
});
}
//*********************************** */
async createReferral(userId: string, queryRunner: QueryRunner) {
const referral = queryRunner.manager.create(this.referralsRepository.target, {
referrer: { id: userId },
referredUser: { id: userId },
});
await queryRunner.manager.save(referral);
}
//*********************************** */
async createReferralReward(userId: string, referralId: string, amount: Decimal, queryRunner: QueryRunner) {
const reward = queryRunner.manager.create(this.referralRewardsRepository.target, {
user: { id: userId },
referral: { id: referralId },
amount,
});
await queryRunner.manager.save(reward);
return reward;
}
//*********************************** */
private generateRandomCode(): string {
return randomBytes(6).toString("hex");
}
+19 -27
View File
@@ -1,31 +1,23 @@
import { Body, Controller, Post } from "@nestjs/common";
import { ApiOperation } from "@nestjs/swagger";
// import { Body, Controller, Post } from "@nestjs/common";
import { UseReferralCodeDto } from "./DTO/use-referral-code.dto";
import { ReferralsService } from "./providers/referrals.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
// import { UserDec } from "../../common/decorators/user.decorator";
// import { ReferralsService } from "./providers/referrals.service";
// import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
// // import { UserDec } from "../../common/decorators/user.decorator";
@Controller("referrals")
@AuthGuards()
export class ReferralsController {
constructor(private readonly referralsService: ReferralsService) {}
// @Controller("referrals")
// @AuthGuards()
// export class ReferralsController {
// constructor(private readonly referralsService: ReferralsService) {}
@ApiOperation({ summary: "Use a referral code" })
@Post("use-code")
useReferralCode(@Body() dto: UseReferralCodeDto) {
return this.referralsService.useReferralCode(dto);
}
// // @ApiOperation({ summary: "Get referral statistics for a user" })
// // @Get("stats/:userId")
// // getReferralStats(@Param("userId") userId: string) {
// // return this.referralsService.getReferralStats(userId);
// // }
// @ApiOperation({ summary: "Get referral statistics for a user" })
// @Get("stats/:userId")
// getReferralStats(@Param("userId") userId: string) {
// return this.referralsService.getReferralStats(userId);
// }
// @Get("my-stats")
// @ApiOperation({ summary: "Get current user's referral statistics" })
// getMyReferralStats(@UserDec("id") userId: string) {
// return this.referralsService.getReferralStats(userId);
// }
}
// // @Get("my-stats")
// // @ApiOperation({ summary: "Get current user's referral statistics" })
// // getMyReferralStats(@UserDec("id") userId: string) {
// // return this.referralsService.getReferralStats(userId);
// // }
// }
+2 -4
View File
@@ -5,16 +5,14 @@ import { ReferralCode } from "./entities/referral-code.entity";
import { ReferralReward } from "./entities/referral-reward.entity";
import { Referral } from "./entities/referral.entity";
import { ReferralsService } from "./providers/referrals.service";
import { ReferralsController } from "./referrals.controller";
import { WalletsModule } from "../wallets/wallets.module";
import { ReferralCodesRepository } from "./repositories/referral-codes.repository";
import { ReferralRewardsRepository } from "./repositories/referral-rewards.repository";
import { ReferralsRepository } from "./repositories/referrals.repository";
@Module({
imports: [TypeOrmModule.forFeature([Referral, ReferralReward, ReferralCode]), WalletsModule],
imports: [TypeOrmModule.forFeature([Referral, ReferralReward, ReferralCode])],
providers: [ReferralsService, ReferralsRepository, ReferralCodesRepository, ReferralRewardsRepository],
controllers: [ReferralsController],
controllers: [],
exports: [ReferralsService],
})
export class ReferralsModule {}