diff --git a/src/modules/auth/transformers/user-login.transformer.ts b/src/modules/auth/transformers/user-login.transformer.ts index f6eaa9b..6148def 100644 --- a/src/modules/auth/transformers/user-login.transformer.ts +++ b/src/modules/auth/transformers/user-login.transformer.ts @@ -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, diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index 0f4441a..b32317a 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -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, { diff --git a/src/modules/users/controllers/users.controller.ts b/src/modules/users/controllers/users.controller.ts index ad163ba..c8bea6d 100644 --- a/src/modules/users/controllers/users.controller.ts +++ b/src/modules/users/controllers/users.controller.ts @@ -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', }; } diff --git a/src/modules/users/dto/convert-score-to-wallet.dto.ts b/src/modules/users/dto/convert-score-to-wallet.dto.ts deleted file mode 100644 index 2414bc0..0000000 --- a/src/modules/users/dto/convert-score-to-wallet.dto.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/users/dto/update-user.dto.ts b/src/modules/users/dto/update-user.dto.ts index 56d8aa8..0675aff 100644 --- a/src/modules/users/dto/update-user.dto.ts +++ b/src/modules/users/dto/update-user.dto.ts @@ -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; } diff --git a/src/modules/users/entities/user-wallet.entity.ts b/src/modules/users/entities/user-wallet.entity.ts new file mode 100644 index 0000000..977e45b --- /dev/null +++ b/src/modules/users/entities/user-wallet.entity.ts @@ -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; +} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 17ac185..e28282c 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -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], diff --git a/src/modules/users/providers/user.service.ts b/src/modules/users/providers/user.service.ts index 8e0a7d4..95766fa 100644 --- a/src/modules/users/providers/user.service.ts +++ b/src/modules/users/providers/user.service.ts @@ -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 { + async updateUser(userId: string, restId: string, dto: UpdateUserDto): Promise { 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 { + 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 { + return this.userWalletRepository.findOne({ user: { id: userId }, restaurant: { id: restId } }); + } } diff --git a/src/modules/users/repositories/user-wallet.repository.ts b/src/modules/users/repositories/user-wallet.repository.ts new file mode 100644 index 0000000..edf2875 --- /dev/null +++ b/src/modules/users/repositories/user-wallet.repository.ts @@ -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 { + constructor(readonly em: EntityManager) { + super(em, UserWallet); + } +} diff --git a/src/modules/users/user.module.ts b/src/modules/users/user.module.ts index 18a2721..03cabec 100644 --- a/src/modules/users/user.module.ts +++ b/src/modules/users/user.module.ts @@ -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 {} diff --git a/src/seeders/DatabaseSeeder.ts b/src/seeders/DatabaseSeeder.ts index b1322c2..e08eb2c 100644 --- a/src/seeders/DatabaseSeeder.ts +++ b/src/seeders/DatabaseSeeder.ts @@ -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); } } diff --git a/src/seeders/user-wallets.seeder.ts b/src/seeders/user-wallets.seeder.ts new file mode 100644 index 0000000..43f0765 --- /dev/null +++ b/src/seeders/user-wallets.seeder.ts @@ -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 { + 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(); + } +} diff --git a/src/seeders/users.seeder.ts b/src/seeders/users.seeder.ts index bde545e..0cb66b2 100644 --- a/src/seeders/users.seeder.ts +++ b/src/seeders/users.seeder.ts @@ -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); }