This commit is contained in:
2025-12-13 12:30:38 +03:30
parent 5d872db4ad
commit 9d9de98c03
13 changed files with 142 additions and 91 deletions
@@ -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 -21
View File
@@ -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;
}
+4 -4
View File
@@ -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],
+26 -14
View File
@@ -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);
}
}
+5 -3
View File
@@ -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 {}
+5
View File
@@ -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);
}
}
+33
View File
@@ -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();
}
}
-2
View File
@@ -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);
}