wallet
This commit is contained in:
@@ -6,8 +6,7 @@ export interface UserLoginResponse {
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
phone: string;
|
||||
wallet: number;
|
||||
points: number;
|
||||
|
||||
isActive?: boolean;
|
||||
restaurant: {
|
||||
id: string;
|
||||
@@ -23,8 +22,6 @@ export class UserLoginTransformer {
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
phone: user.phone,
|
||||
wallet: user.wallet,
|
||||
points: user.points,
|
||||
isActive: user.isActive,
|
||||
restaurant: {
|
||||
id: restaurant.id,
|
||||
|
||||
@@ -61,17 +61,17 @@ export class PaymentsService {
|
||||
const { amount, method, restaurantDomain, gateway, merchantId, user } = await this.validateOrder(orderId);
|
||||
|
||||
// Handle Wallet payment immediately
|
||||
if (method === PaymentMethodEnum.Wallet) {
|
||||
// Check wallet balance
|
||||
if (user.wallet < amount) {
|
||||
throw new BadRequestException('Insufficient wallet balance');
|
||||
}
|
||||
// if (method === PaymentMethodEnum.Wallet) {
|
||||
// // Check wallet balance
|
||||
// if (user.wallet < amount) {
|
||||
// throw new BadRequestException('Insufficient wallet balance');
|
||||
// }
|
||||
|
||||
// Deduct from wallet and create payment record
|
||||
const payment = await this.processWalletPayment(orderId, amount, user);
|
||||
// // Deduct from wallet and create payment record
|
||||
// const payment = await this.processWalletPayment(orderId, amount, user);
|
||||
|
||||
return { paymentUrl: null }; // No redirect needed for wallet
|
||||
}
|
||||
// return { paymentUrl: null }; // No redirect needed for wallet
|
||||
// }
|
||||
|
||||
const { authority } = await this.createPayment(restaurantDomain, {
|
||||
amount,
|
||||
@@ -149,13 +149,13 @@ export class PaymentsService {
|
||||
}
|
||||
|
||||
// Double-check balance
|
||||
if (freshUser.wallet < amount) {
|
||||
throw new BadRequestException('Insufficient wallet balance');
|
||||
}
|
||||
// if (freshUser.wallet < amount) {
|
||||
// throw new BadRequestException('Insufficient wallet balance');
|
||||
// }
|
||||
|
||||
// Deduct from wallet
|
||||
freshUser.wallet -= amount;
|
||||
em.persist(freshUser);
|
||||
// freshUser.wallet -= amount;
|
||||
// em.persist(freshUser);
|
||||
|
||||
// Create payment record
|
||||
const payment = em.create(Payment, {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { UserId } from 'src/common/decorators/user-id.decorator';
|
||||
import { RestId } from 'src/common/decorators';
|
||||
import { FindUsersDto } from '../dto/find-user.dto';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { ConvertScoreToWalletDto } from '../dto/convert-score-to-wallet.dto';
|
||||
|
||||
@ApiTags('User')
|
||||
@Controller()
|
||||
@@ -45,11 +44,8 @@ export class UsersController {
|
||||
})
|
||||
@ApiBody({ type: UpdateUserDto })
|
||||
@Patch('public/user/update')
|
||||
async updateUser(
|
||||
@UserId() userId: string,
|
||||
@Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserDto,
|
||||
) {
|
||||
const user = await this.userService.updateUser(userId, dto);
|
||||
async updateUser(@UserId() userId: string, @RestId() restId: string, @Body() dto: UpdateUserDto) {
|
||||
const user = await this.userService.updateUser(userId, restId, dto);
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -71,6 +67,23 @@ export class UsersController {
|
||||
return addresses;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Get User Wallet' })
|
||||
@ApiHeader({
|
||||
name: 'X-Slug',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
default: 'zhivan',
|
||||
},
|
||||
})
|
||||
@Get('public/user/wallet')
|
||||
async getUserWallet(@UserId() userId: string, @RestId() restId: string) {
|
||||
const wallet = await this.userService.getUserWallet(userId, restId);
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Create a new address for the authenticated user' })
|
||||
@@ -194,14 +207,10 @@ export class UsersController {
|
||||
default: 'zhivan',
|
||||
},
|
||||
})
|
||||
@ApiBody({ type: ConvertScoreToWalletDto })
|
||||
@Post('public/user/convert-score-to-wallet')
|
||||
async convertScoreToWallet(@UserId() userId: string, @Body() dto: ConvertScoreToWalletDto, @RestId() restId: string) {
|
||||
const user = await this.userService.convertScoreToWallet(userId, restId, dto);
|
||||
async convertScoreToWallet(@UserId() userId: string, @RestId() restId: string) {
|
||||
await this.userService.convertScoreToWallet(userId, restId);
|
||||
return {
|
||||
id: user.id,
|
||||
wallet: user.wallet,
|
||||
points: user.points,
|
||||
message: 'Score converted to wallet successfully',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { IsNumber, IsOptional, Min } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* DTO for converting user score (points) to wallet balance.
|
||||
*/
|
||||
export class ConvertScoreToWalletDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Amount of points to convert. If not provided, converts all available points',
|
||||
example: 100,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
points?: number;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsString, IsOptional, IsBoolean, IsNumber } from 'class-validator';
|
||||
import { IsString, IsOptional, IsBoolean } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
@@ -28,28 +28,8 @@ export class UpdateUserDto {
|
||||
@IsString()
|
||||
marriageDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Referrer's identifier (optional)", example: 'abc123' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referrer?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Is the user active?', example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Gender flag (boolean)', example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
gender?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Wallet balance (integer)', example: 0 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
wallet?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Reward points (integer)', example: 0 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
points?: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Entity, Property, ManyToOne, Index } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { User } from './user.entity';
|
||||
|
||||
@Entity({ tableName: 'user_wallets' })
|
||||
@Index({ properties: ['user', 'restaurant'] }) // Composite index for most common query: find wallet by user and restaurant
|
||||
@Index({ properties: ['user'] }) // Index for queries finding all wallets for a user
|
||||
@Index({ properties: ['restaurant'] }) // Index for queries finding all wallets for a restaurant
|
||||
export class UserWallet extends BaseEntity {
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant!: Restaurant;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
user!: User;
|
||||
|
||||
@Property({ default: 0, type: 'int' })
|
||||
wallet: number = 0;
|
||||
|
||||
@Property({ default: 0, type: 'int' })
|
||||
points: number = 0;
|
||||
}
|
||||
@@ -42,11 +42,11 @@ export class User extends BaseEntity {
|
||||
@Property({ default: true })
|
||||
gender?: boolean;
|
||||
|
||||
@Property({ default: 0, type: 'int' })
|
||||
wallet: number = 0;
|
||||
// @Property({ default: 0, type: 'int' })
|
||||
// wallet: number = 0;
|
||||
|
||||
@Property({ default: 0, type: 'int' })
|
||||
points: number = 0;
|
||||
// @Property({ default: 0, type: 'int' })
|
||||
// points: number = 0;
|
||||
|
||||
@OneToMany(() => UserAddress, address => address.user, {
|
||||
cascade: [Cascade.ALL],
|
||||
|
||||
@@ -10,14 +10,16 @@ import { UpdateUserAddressDto } from '../dto/update-user-address.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
import { ConvertScoreToWalletDto } from '../dto/convert-score-to-wallet.dto';
|
||||
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
|
||||
import { UserWallet } from '../entities/user-wallet.entity';
|
||||
import { UserWalletRepository } from '../repositories/user-wallet.repository';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly restaurantRepository: RestRepository,
|
||||
private readonly userWalletRepository: UserWalletRepository,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
@@ -85,7 +87,7 @@ export class UserService {
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
|
||||
async updateUser(userId: string, restId: string, dto: UpdateUserDto): Promise<User> {
|
||||
const user = await this.userRepository.findOne({ id: userId });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
@@ -99,7 +101,7 @@ export class UserService {
|
||||
if (dto.marriageDate) {
|
||||
assignData.marriageDate = new Date(dto.marriageDate);
|
||||
}
|
||||
|
||||
// get restuarant
|
||||
this.em.assign(user, assignData);
|
||||
await this.em.flush();
|
||||
|
||||
@@ -287,27 +289,34 @@ export class UserService {
|
||||
return address;
|
||||
}
|
||||
|
||||
async convertScoreToWallet(userId: string, restId: string, dto: ConvertScoreToWalletDto): Promise<User> {
|
||||
async convertScoreToWallet(userId: string, restId: string) {
|
||||
const user = await this.userRepository.findOne({ id: userId });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
}
|
||||
|
||||
const restaurant = await this.restaurantRepository.findOne({ id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(`Restaurant with ID ${restId} not found.`);
|
||||
}
|
||||
let userWallet = await this.userWalletRepository.findOne({ user: { id: userId }, restaurant: { id: restId } });
|
||||
if (!userWallet) {
|
||||
userWallet = this.userWalletRepository.create({ user: user, restaurant, wallet: 0, points: 0 });
|
||||
await this.em.persistAndFlush(userWallet);
|
||||
}
|
||||
// Determine how many points to convert
|
||||
const pointsToConvert = dto.points ?? user.points;
|
||||
const pointsToConvert = userWallet.points;
|
||||
|
||||
// Validate points to convert
|
||||
if (pointsToConvert <= 0) {
|
||||
throw new BadRequestException('Points to convert must be greater than 0.');
|
||||
}
|
||||
|
||||
if (user.points < pointsToConvert) {
|
||||
throw new BadRequestException(`Insufficient points. Available: ${user.points}, Requested: ${pointsToConvert}.`);
|
||||
}
|
||||
const restaurant = await this.restaurantRepository.findOne({ id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(`Restaurant with ID ${restId} not found.`);
|
||||
if (userWallet.points < pointsToConvert) {
|
||||
throw new BadRequestException(
|
||||
`Insufficient points. Available: ${userWallet.points}, Requested: ${pointsToConvert}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!restaurant.score) {
|
||||
throw new BadRequestException('Restaurant score not found.');
|
||||
}
|
||||
@@ -322,11 +331,14 @@ export class UserService {
|
||||
// Convert points to wallet (1:1 ratio - can be made configurable)
|
||||
|
||||
// Update user's wallet and points
|
||||
user.wallet = (user.wallet || 0) + walletIncreaseAmount;
|
||||
user.points = user.points - pointsToConvert;
|
||||
userWallet.wallet = (userWallet.wallet || 0) + walletIncreaseAmount;
|
||||
userWallet.points = (userWallet.points || 0) - pointsToConvert;
|
||||
|
||||
await this.em.flush();
|
||||
|
||||
return user;
|
||||
}
|
||||
getUserWallet(userId: string, restId: string): Promise<UserWallet | null> {
|
||||
return this.userWalletRepository.findOne({ user: { id: userId }, restaurant: { id: restId } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { UserWallet } from '../entities/user-wallet.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserWalletRepository extends EntityRepository<UserWallet> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, UserWallet);
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,13 @@ import { UserAddress } from './entities/user-address.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { UserWalletRepository } from './repositories/user-wallet.repository';
|
||||
import { UserWallet } from './entities/user-wallet.entity';
|
||||
|
||||
@Module({
|
||||
providers: [UserService, UserRepository],
|
||||
providers: [UserService, UserRepository, UserWalletRepository],
|
||||
controllers: [UsersController],
|
||||
imports: [MikroOrmModule.forFeature([User, UserAddress]), JwtModule, RestaurantsModule],
|
||||
exports: [UserService, UserRepository],
|
||||
imports: [MikroOrmModule.forFeature([User, UserAddress, UserWallet]), JwtModule, RestaurantsModule],
|
||||
exports: [UserService, UserRepository, UserWalletRepository],
|
||||
})
|
||||
export class UserModule {}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AdminsSeeder } from './admins.seeder';
|
||||
import { UsersSeeder } from './users.seeder';
|
||||
import { CouponsSeeder } from './coupons.seeder';
|
||||
import { NotificationPreferencesSeeder } from './notification-preferences.seeder';
|
||||
import { UserWalletsSeeder } from './user-wallets.seeder';
|
||||
|
||||
export class DatabaseSeeder extends Seeder {
|
||||
async run(em: EntityManager) {
|
||||
@@ -64,5 +65,9 @@ export class DatabaseSeeder extends Seeder {
|
||||
// 12. Create Users
|
||||
const usersSeeder = new UsersSeeder();
|
||||
await usersSeeder.run(em);
|
||||
|
||||
// 13. Create User Wallets
|
||||
const userWalletsSeeder = new UserWalletsSeeder();
|
||||
await userWalletsSeeder.run(em);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { EntityManager } from '@mikro-orm/core';
|
||||
import { User } from '../modules/users/entities/user.entity';
|
||||
import { Restaurant } from '../modules/restaurants/entities/restaurant.entity';
|
||||
import { UserWallet } from '../modules/users/entities/user-wallet.entity';
|
||||
|
||||
export class UserWalletsSeeder {
|
||||
async run(em: EntityManager): Promise<void> {
|
||||
const users = await em.find(User, {});
|
||||
const restaurants = await em.find(Restaurant, {});
|
||||
|
||||
for (const user of users) {
|
||||
for (const restaurant of restaurants) {
|
||||
// Check if wallet already exists for this user-restaurant combination
|
||||
const existingWallet = await em.findOne(UserWallet, {
|
||||
user: { id: user.id },
|
||||
restaurant: { id: restaurant.id },
|
||||
});
|
||||
|
||||
if (!existingWallet) {
|
||||
const wallet = em.create(UserWallet, {
|
||||
user,
|
||||
restaurant,
|
||||
wallet: 150000,
|
||||
points: 125,
|
||||
});
|
||||
em.persist(wallet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await em.flush();
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,6 @@ export class UsersSeeder {
|
||||
marriageDate: new Date(userData.marriageDate),
|
||||
isActive: userData.isActive,
|
||||
gender: userData.gender,
|
||||
wallet: userData.wallet,
|
||||
points: userData.points,
|
||||
});
|
||||
em.persist(user);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user