chore: add referral module

This commit is contained in:
mahyargdz
2025-04-14 16:26:37 +03:30
parent 99ca2461f1
commit bec73658d5
21 changed files with 378 additions and 8 deletions
+2
View File
@@ -31,6 +31,7 @@ import { LearningModule } from "./modules/learnings/learning.module";
// import { LoggerModule } from "./modules/logger/logger.module";
import { NotificationModule } from "./modules/notifications/notifications.module";
import { PaymentsModule } from "./modules/payments/payments.module";
import { ReferralsModule } from "./modules/referrals/referrals.module";
import { SettingModule } from "./modules/settings/settings.module";
import { SubscriptionsModule } from "./modules/subscriptions/subscriptions.module";
import { TicketsModule } from "./modules/tickets/tickets.module";
@@ -70,6 +71,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module";
BlogsModule,
// LoggerModule,
LandingModule,
ReferralsModule,
],
controllers: [],
})
+11
View File
@@ -292,6 +292,7 @@ export const enum WalletMessage {
SUBSCRIPTION_WALLET_TRANSFER = "پرداخت اشتراک از طریق کیف پول",
INVOICE_WALLET_TRANSFER = "پرداخت صورت حساب از طریق کیف پول",
TRANSACTION_NOT_FOUND = "تراکنش پیدا نشد",
REFERRAL_REWARD_WALLET_TRANSFER = "پرداخت پاداش ارجاع از طریق کیف پول",
}
export const enum PaymentMessage {
@@ -689,3 +690,13 @@ export const enum SliderMessage {
IS_ACTIVE_SHOULD_BE_A_BOOLEAN = "وضعیت فعال بودن اسلایدر باید یک بولین باشد",
SLIDER_NOT_FOUND = "اسلایدر مورد نظر یافت نشد",
}
export const enum ReferralMessage {
CODE_EXIST_FOR_USER = "کد ارجاع برای این کاربر قبلاً وجود دارد",
CODE_GENERATION_FAILED = "تولید کد ارجاع منحصر به فرد پس از چندین تلاش ناموفق بود",
INVALID_REFERRAL_CODE = "کد ارجاع نامعتبر است",
REFERRAL_ALREADY_USED = "کد ارجاع قبلاً استفاده شده است",
REFERRAL_SUCCESS = "کد ارجاع با موفقیت اعمال شد",
INVALID_OR_INACTIVE_REFERRAL_CODE = "کد ارجاع نامعتبر یا غیرفعال است",
USER_ALREADY_REFERRED = "کاربر قبلاً ارجاع داده شده است",
}
+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: true,
synchronize: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
logging: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
migrationsTableName: "typeorm_migrations",
migrationsRun: false,
@@ -149,6 +149,7 @@ export class BlogsController {
}
@ApiOperation({ summary: "Get all active categories (user side)" })
@SkipAuth()
@Get("categories/public")
getCategoriesUserSide() {
return this.blogsService.getCategoriesUserSide();
@@ -21,7 +21,7 @@ export class Discount extends BaseEntity {
@Column({ type: "enum", enum: DiscountCalculationType })
calculationType: DiscountCalculationType;
@Column({ type: "decimal", precision: 10, scale: 2, nullable: false, transformer: new DecimalTransformer() })
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
amount: Decimal;
@Column({ type: "boolean", default: true })
@@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from "class-validator";
export class UseReferralCodeDto {
@IsNotEmpty()
@IsString()
referralCode: string;
@IsNotEmpty()
@IsString()
userId: string;
}
@@ -0,0 +1,28 @@
import { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm";
import { Referral } from "./referral.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
@Entity()
export class ReferralCode extends BaseEntity {
@Column({ type: "varchar", length: 6, unique: true })
code: string;
@Column({ type: "boolean", default: true })
isActive: boolean;
@Column({ type: "integer", default: 0 })
usedCount: number;
@Column({ type: "timestamptz", nullable: true })
expiresAt: Date;
//*********************************** */
@OneToOne(() => User, (user) => user.referralCode)
@JoinColumn()
user: User;
@OneToMany(() => Referral, (referral) => referral.referralCode)
referrals: Referral[];
}
@@ -0,0 +1,29 @@
import Decimal from "decimal.js";
import { Column, Entity, ManyToOne } from "typeorm";
import { Referral } from "./referral.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
import { User } from "../../users/entities/user.entity";
import { ReferralRewardStatus } from "../enums/referral-reward-status.enum";
@Entity()
export class ReferralReward extends BaseEntity {
@ManyToOne(() => User, (user) => user.referralRewards)
user: User;
@ManyToOne(() => Referral, (referral) => referral.rewards)
referral: Referral;
@Column({ type: "decimal", precision: 10, scale: 2, nullable: false, transformer: new DecimalTransformer() })
amount: Decimal;
@Column({ type: "enum", enum: ReferralRewardStatus, default: ReferralRewardStatus.PENDING })
status: ReferralRewardStatus;
@Column({ type: "timestamp", nullable: true })
paidAt: Date;
@Column({ type: "varchar", length: 255, nullable: true })
transactionId: string;
}
@@ -0,0 +1,28 @@
import { Column, Entity, ManyToOne, OneToMany } from "typeorm";
import { ReferralCode } from "./referral-code.entity";
import { ReferralReward } from "./referral-reward.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
import { ReferralStatus } from "../enums/referral-status.enum";
@Entity()
export class Referral extends BaseEntity {
@ManyToOne(() => User, (user) => user.referrals)
referrer: User;
@ManyToOne(() => User, (user) => user.referredBy)
referredUser: User;
@ManyToOne(() => ReferralCode, (code) => code.referrals)
referralCode: ReferralCode;
@OneToMany(() => ReferralReward, (reward) => reward.referral)
rewards: ReferralReward[];
@Column({ type: "enum", enum: ReferralStatus, default: ReferralStatus.PENDING })
status: ReferralStatus;
@Column({ type: "timestamptz", nullable: true })
completedAt?: Date;
}
@@ -0,0 +1,5 @@
export enum ReferralRewardStatus {
PENDING = "PENDING",
PAID = "PAID",
CANCELLED = "CANCELLED",
}
@@ -0,0 +1,5 @@
export enum ReferralStatus {
PENDING = "PENDING",
COMPLETED = "COMPLETED",
CANCELLED = "CANCELLED",
}
@@ -0,0 +1,128 @@
import { randomBytes } from "node:crypto";
import { BadRequestException, Injectable } from "@nestjs/common";
import Decimal from "decimal.js";
import { DataSource, 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";
import { ReferralsRepository } from "../repositories/referrals.repository";
@Injectable()
export class ReferralsService {
constructor(
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) {
const existReferralCode = await queryRunner.manager.findOneBy(this.referralCodesRepository.target, { user: { id: userId } });
if (existReferralCode) throw new BadRequestException(ReferralMessage.CODE_EXIST_FOR_USER);
const MAX_ATTEMPTS = 10;
let attempts = 0;
let code: string;
let existCode;
do {
if (attempts >= MAX_ATTEMPTS) {
throw new BadRequestException(ReferralMessage.CODE_GENERATION_FAILED);
}
code = this.generateRandomCode();
existCode = await queryRunner.manager.findOneBy(this.referralCodesRepository.target, { code });
attempts++;
} while (existCode);
const referralCode = queryRunner.manager.create(this.referralCodesRepository.target, {
code,
user: { id: userId },
});
await queryRunner.manager.save(referralCode);
return code;
}
//*********************************** */
async useReferralCode(dto: UseReferralCodeDto) {
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const referredUser = await queryRunner.manager.findOne(User, { where: { id: dto.userId } });
if (!referredUser) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
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);
// 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);
// 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,
});
await queryRunner.manager.save(referral);
// Increment usedCount
referralCode.usedCount += 1;
await queryRunner.manager.save(referralCode);
// 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();
}
}
private generateRandomCode(): string {
return randomBytes(6).toString("hex");
}
}
@@ -0,0 +1,31 @@
import { Body, Controller, Post } from "@nestjs/common";
import { ApiOperation } from "@nestjs/swagger";
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";
@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);
// }
// @Get("my-stats")
// @ApiOperation({ summary: "Get current user's referral statistics" })
// getMyReferralStats(@UserDec("id") userId: string) {
// return this.referralsService.getReferralStats(userId);
// }
}
+20
View File
@@ -0,0 +1,20 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
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],
providers: [ReferralsService, ReferralsRepository, ReferralCodesRepository, ReferralRewardsRepository],
controllers: [ReferralsController],
exports: [ReferralsService],
})
export class ReferralsModule {}
@@ -0,0 +1,12 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { ReferralCode } from "../entities/referral-code.entity";
@Injectable()
export class ReferralCodesRepository extends Repository<ReferralCode> {
constructor(@InjectRepository(ReferralCode) repository: Repository<ReferralCode>) {
super(repository.target, repository.manager, repository.queryRunner);
}
}
@@ -0,0 +1,12 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { ReferralReward } from "../entities/referral-reward.entity";
@Injectable()
export class ReferralRewardsRepository extends Repository<ReferralReward> {
constructor(@InjectRepository(ReferralReward) repository: Repository<ReferralReward>) {
super(repository.target, repository.manager, repository.queryRunner);
}
}
@@ -0,0 +1,12 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Referral } from "../entities/referral.entity";
@Injectable()
export class ReferralsRepository extends Repository<Referral> {
constructor(@InjectRepository(Referral) repository: Repository<Referral>) {
super(repository.target, repository.manager, repository.queryRunner);
}
}
+15 -4
View File
@@ -18,6 +18,9 @@ import { Invoice } from "../../invoices/entities/invoice.entity";
import { LearningProgress } from "../../learnings/entities/learning-progress.entity";
import { Notification } from "../../notifications/entities/notification.entity";
import { Payment } from "../../payments/entities/payment.entity";
import { ReferralCode } from "../../referrals/entities/referral-code.entity";
import { ReferralReward } from "../../referrals/entities/referral-reward.entity";
import { Referral } from "../../referrals/entities/referral.entity";
import { UserSetting } from "../../settings/entities/user-setting.entity";
import { UserQuickAccess } from "../../subscriptions/entities/user-quick-access.entity";
import { UserSubscription } from "../../subscriptions/entities/user-subscription.entity";
@@ -132,8 +135,16 @@ export class User extends BaseEntity {
@OneToMany(() => RefreshToken, (refreshToken) => refreshToken.user, { cascade: true })
refreshTokens: RefreshToken[];
}
// @ManyToMany(() => DanakService, (danakService) => danakService.users)
// @JoinTable()
// danakServices: DanakService[];
@OneToOne(() => ReferralCode, (code) => code.user)
referralCode: ReferralCode;
@OneToMany(() => Referral, (referral) => referral.referrer)
referrals: Referral[];
@OneToMany(() => Referral, (referral) => referral.referredUser)
referredBy: Referral[];
@OneToMany(() => ReferralReward, (reward) => reward.user)
referralRewards: ReferralReward[];
}
+3 -2
View File
@@ -11,6 +11,7 @@ import { AddressService } from "../../address/providers/address.service";
import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto";
import { RequestOtpDto } from "../../auth/DTO/request-otp.dto";
import { VerifyOtpDto } from "../../auth/DTO/verify-otp.dto";
import { ReferralsService } from "../../referrals/providers/referrals.service";
import { UserSettingsService } from "../../settings/providers/user-settings.service";
import { CacheService } from "../../utils/providers/cache.service";
import { EmailService } from "../../utils/providers/email.service";
@@ -30,7 +31,6 @@ import { LegalUserRepository } from "../repositories/legal-user.repository";
import { RealUserRepository } from "../repositories/real-user.repository";
import { UserGroupRepository } from "../repositories/user-group.repository";
import { UserRepository } from "../repositories/users.repository";
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
@@ -47,6 +47,7 @@ export class UsersService {
private readonly otpService: OTPService,
private readonly smsService: SmsService,
private readonly cacheService: CacheService,
private readonly referralsService: ReferralsService,
) {}
/************************************************************ */
@@ -254,8 +255,8 @@ export class UsersService {
await queryRunner.manager.save(user);
await this.userSettingsService.createUserSettings(user.id, queryRunner);
await this.walletsService.createUserWallet(user.id, queryRunner);
await this.referralsService.generateReferralCode(user.id, queryRunner);
return user;
}
+2
View File
@@ -26,6 +26,7 @@ import { UtilsModule } from "../utils/utils.module";
import { AdminsService } from "./providers/admins.service";
import { CustomersService } from "./providers/customers.service";
import { RefreshTokensRepository } from "./repositories/refresh-token.repository";
import { ReferralsModule } from "../referrals/referrals.module";
@Module({
imports: [
@@ -33,6 +34,7 @@ import { RefreshTokensRepository } from "./repositories/refresh-token.repository
WalletsModule,
UtilsModule,
AddressModule,
ReferralsModule,
],
providers: [
UsersService,
@@ -111,6 +111,27 @@ export class WalletsService {
return transaction;
}
//*********************************** */
async createReferralRewardTransaction(userId: string, amount: Decimal, queryRunner: QueryRunner) {
const wallet = await queryRunner.manager.findOne(Wallet, { where: { user: { id: userId } }, lock: { mode: "pessimistic_write" } });
if (!wallet) throw new BadRequestException(WalletMessage.WALLET_NOT_FOUND);
const transaction = queryRunner.manager.create(WalletTransaction, {
amount,
wallet: { id: wallet.id },
type: TransactionType.DEPOSIT,
description: WalletMessage.REFERRAL_REWARD_WALLET_TRANSFER,
});
wallet.balance = new Decimal(wallet.balance).add(transaction.amount);
await queryRunner.manager.save(Wallet, wallet);
await queryRunner.manager.save(WalletTransaction, transaction);
return transaction;
}
//*********************************** */
async getTransaction(userId: string, paginationDto: PaginationDto) {