wallet
This commit is contained in:
@@ -6,8 +6,7 @@ export interface UserLoginResponse {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
wallet: number;
|
|
||||||
points: number;
|
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
restaurant: {
|
restaurant: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -23,8 +22,6 @@ export class UserLoginTransformer {
|
|||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
phone: user.phone,
|
phone: user.phone,
|
||||||
wallet: user.wallet,
|
|
||||||
points: user.points,
|
|
||||||
isActive: user.isActive,
|
isActive: user.isActive,
|
||||||
restaurant: {
|
restaurant: {
|
||||||
id: restaurant.id,
|
id: restaurant.id,
|
||||||
|
|||||||
@@ -61,17 +61,17 @@ export class PaymentsService {
|
|||||||
const { amount, method, restaurantDomain, gateway, merchantId, user } = await this.validateOrder(orderId);
|
const { amount, method, restaurantDomain, gateway, merchantId, user } = await this.validateOrder(orderId);
|
||||||
|
|
||||||
// Handle Wallet payment immediately
|
// Handle Wallet payment immediately
|
||||||
if (method === PaymentMethodEnum.Wallet) {
|
// if (method === PaymentMethodEnum.Wallet) {
|
||||||
// Check wallet balance
|
// // Check wallet balance
|
||||||
if (user.wallet < amount) {
|
// if (user.wallet < amount) {
|
||||||
throw new BadRequestException('Insufficient wallet balance');
|
// throw new BadRequestException('Insufficient wallet balance');
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Deduct from wallet and create payment record
|
// // Deduct from wallet and create payment record
|
||||||
const payment = await this.processWalletPayment(orderId, amount, user);
|
// 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, {
|
const { authority } = await this.createPayment(restaurantDomain, {
|
||||||
amount,
|
amount,
|
||||||
@@ -149,13 +149,13 @@ export class PaymentsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Double-check balance
|
// Double-check balance
|
||||||
if (freshUser.wallet < amount) {
|
// if (freshUser.wallet < amount) {
|
||||||
throw new BadRequestException('Insufficient wallet balance');
|
// throw new BadRequestException('Insufficient wallet balance');
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Deduct from wallet
|
// Deduct from wallet
|
||||||
freshUser.wallet -= amount;
|
// freshUser.wallet -= amount;
|
||||||
em.persist(freshUser);
|
// em.persist(freshUser);
|
||||||
|
|
||||||
// Create payment record
|
// Create payment record
|
||||||
const payment = em.create(Payment, {
|
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 { RestId } from 'src/common/decorators';
|
||||||
import { FindUsersDto } from '../dto/find-user.dto';
|
import { FindUsersDto } from '../dto/find-user.dto';
|
||||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||||
import { ConvertScoreToWalletDto } from '../dto/convert-score-to-wallet.dto';
|
|
||||||
|
|
||||||
@ApiTags('User')
|
@ApiTags('User')
|
||||||
@Controller()
|
@Controller()
|
||||||
@@ -45,11 +44,8 @@ export class UsersController {
|
|||||||
})
|
})
|
||||||
@ApiBody({ type: UpdateUserDto })
|
@ApiBody({ type: UpdateUserDto })
|
||||||
@Patch('public/user/update')
|
@Patch('public/user/update')
|
||||||
async updateUser(
|
async updateUser(@UserId() userId: string, @RestId() restId: string, @Body() dto: UpdateUserDto) {
|
||||||
@UserId() userId: string,
|
const user = await this.userService.updateUser(userId, restId, dto);
|
||||||
@Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserDto,
|
|
||||||
) {
|
|
||||||
const user = await this.userService.updateUser(userId, dto);
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +67,23 @@ export class UsersController {
|
|||||||
return addresses;
|
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)
|
@UseGuards(AuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'Create a new address for the authenticated user' })
|
@ApiOperation({ summary: 'Create a new address for the authenticated user' })
|
||||||
@@ -194,14 +207,10 @@ export class UsersController {
|
|||||||
default: 'zhivan',
|
default: 'zhivan',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ApiBody({ type: ConvertScoreToWalletDto })
|
|
||||||
@Post('public/user/convert-score-to-wallet')
|
@Post('public/user/convert-score-to-wallet')
|
||||||
async convertScoreToWallet(@UserId() userId: string, @Body() dto: ConvertScoreToWalletDto, @RestId() restId: string) {
|
async convertScoreToWallet(@UserId() userId: string, @RestId() restId: string) {
|
||||||
const user = await this.userService.convertScoreToWallet(userId, restId, dto);
|
await this.userService.convertScoreToWallet(userId, restId);
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
|
||||||
wallet: user.wallet,
|
|
||||||
points: user.points,
|
|
||||||
message: 'Score converted to wallet successfully',
|
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';
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,28 +28,8 @@ export class UpdateUserDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
marriageDate?: string;
|
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 })
|
@ApiPropertyOptional({ description: 'Gender flag (boolean)', example: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
gender?: boolean;
|
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 })
|
@Property({ default: true })
|
||||||
gender?: boolean;
|
gender?: boolean;
|
||||||
|
|
||||||
@Property({ default: 0, type: 'int' })
|
// @Property({ default: 0, type: 'int' })
|
||||||
wallet: number = 0;
|
// wallet: number = 0;
|
||||||
|
|
||||||
@Property({ default: 0, type: 'int' })
|
// @Property({ default: 0, type: 'int' })
|
||||||
points: number = 0;
|
// points: number = 0;
|
||||||
|
|
||||||
@OneToMany(() => UserAddress, address => address.user, {
|
@OneToMany(() => UserAddress, address => address.user, {
|
||||||
cascade: [Cascade.ALL],
|
cascade: [Cascade.ALL],
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ import { UpdateUserAddressDto } from '../dto/update-user-address.dto';
|
|||||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||||
import { UserRepository } from '../repositories/user.repository';
|
import { UserRepository } from '../repositories/user.repository';
|
||||||
import { normalizePhone } from '../../utils/phone.util';
|
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 { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
|
||||||
|
import { UserWallet } from '../entities/user-wallet.entity';
|
||||||
|
import { UserWalletRepository } from '../repositories/user-wallet.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
private readonly restaurantRepository: RestRepository,
|
private readonly restaurantRepository: RestRepository,
|
||||||
|
private readonly userWalletRepository: UserWalletRepository,
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ export class UserService {
|
|||||||
return user;
|
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 });
|
const user = await this.userRepository.findOne({ id: userId });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||||
@@ -99,7 +101,7 @@ export class UserService {
|
|||||||
if (dto.marriageDate) {
|
if (dto.marriageDate) {
|
||||||
assignData.marriageDate = new Date(dto.marriageDate);
|
assignData.marriageDate = new Date(dto.marriageDate);
|
||||||
}
|
}
|
||||||
|
// get restuarant
|
||||||
this.em.assign(user, assignData);
|
this.em.assign(user, assignData);
|
||||||
await this.em.flush();
|
await this.em.flush();
|
||||||
|
|
||||||
@@ -287,27 +289,34 @@ export class UserService {
|
|||||||
return address;
|
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 });
|
const user = await this.userRepository.findOne({ id: userId });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
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
|
// Determine how many points to convert
|
||||||
const pointsToConvert = dto.points ?? user.points;
|
const pointsToConvert = userWallet.points;
|
||||||
|
|
||||||
// Validate points to convert
|
// Validate points to convert
|
||||||
if (pointsToConvert <= 0) {
|
if (pointsToConvert <= 0) {
|
||||||
throw new BadRequestException('Points to convert must be greater than 0.');
|
throw new BadRequestException('Points to convert must be greater than 0.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.points < pointsToConvert) {
|
if (userWallet.points < pointsToConvert) {
|
||||||
throw new BadRequestException(`Insufficient points. Available: ${user.points}, Requested: ${pointsToConvert}.`);
|
throw new BadRequestException(
|
||||||
}
|
`Insufficient points. Available: ${userWallet.points}, Requested: ${pointsToConvert}.`,
|
||||||
const restaurant = await this.restaurantRepository.findOne({ id: restId });
|
);
|
||||||
if (!restaurant) {
|
|
||||||
throw new NotFoundException(`Restaurant with ID ${restId} not found.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!restaurant.score) {
|
if (!restaurant.score) {
|
||||||
throw new BadRequestException('Restaurant score not found.');
|
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)
|
// Convert points to wallet (1:1 ratio - can be made configurable)
|
||||||
|
|
||||||
// Update user's wallet and points
|
// Update user's wallet and points
|
||||||
user.wallet = (user.wallet || 0) + walletIncreaseAmount;
|
userWallet.wallet = (userWallet.wallet || 0) + walletIncreaseAmount;
|
||||||
user.points = user.points - pointsToConvert;
|
userWallet.points = (userWallet.points || 0) - pointsToConvert;
|
||||||
|
|
||||||
await this.em.flush();
|
await this.em.flush();
|
||||||
|
|
||||||
return user;
|
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 { JwtModule } from '@nestjs/jwt';
|
||||||
import { UserRepository } from './repositories/user.repository';
|
import { UserRepository } from './repositories/user.repository';
|
||||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||||
|
import { UserWalletRepository } from './repositories/user-wallet.repository';
|
||||||
|
import { UserWallet } from './entities/user-wallet.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [UserService, UserRepository],
|
providers: [UserService, UserRepository, UserWalletRepository],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
imports: [MikroOrmModule.forFeature([User, UserAddress]), JwtModule, RestaurantsModule],
|
imports: [MikroOrmModule.forFeature([User, UserAddress, UserWallet]), JwtModule, RestaurantsModule],
|
||||||
exports: [UserService, UserRepository],
|
exports: [UserService, UserRepository, UserWalletRepository],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { AdminsSeeder } from './admins.seeder';
|
|||||||
import { UsersSeeder } from './users.seeder';
|
import { UsersSeeder } from './users.seeder';
|
||||||
import { CouponsSeeder } from './coupons.seeder';
|
import { CouponsSeeder } from './coupons.seeder';
|
||||||
import { NotificationPreferencesSeeder } from './notification-preferences.seeder';
|
import { NotificationPreferencesSeeder } from './notification-preferences.seeder';
|
||||||
|
import { UserWalletsSeeder } from './user-wallets.seeder';
|
||||||
|
|
||||||
export class DatabaseSeeder extends Seeder {
|
export class DatabaseSeeder extends Seeder {
|
||||||
async run(em: EntityManager) {
|
async run(em: EntityManager) {
|
||||||
@@ -64,5 +65,9 @@ export class DatabaseSeeder extends Seeder {
|
|||||||
// 12. Create Users
|
// 12. Create Users
|
||||||
const usersSeeder = new UsersSeeder();
|
const usersSeeder = new UsersSeeder();
|
||||||
await usersSeeder.run(em);
|
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),
|
marriageDate: new Date(userData.marriageDate),
|
||||||
isActive: userData.isActive,
|
isActive: userData.isActive,
|
||||||
gender: userData.gender,
|
gender: userData.gender,
|
||||||
wallet: userData.wallet,
|
|
||||||
points: userData.points,
|
|
||||||
});
|
});
|
||||||
em.persist(user);
|
em.persist(user);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user