chore: add referral module
This commit is contained in:
@@ -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);
|
||||
// }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user