user module

This commit is contained in:
2026-01-13 19:59:06 +03:30
parent 6522acff5f
commit d630cb844a
62 changed files with 1137 additions and 3040 deletions
+111 -112
View File
@@ -1,16 +1,16 @@
import { Controller, Get, UseGuards, Patch, Body, ValidationPipe, Post, Query, Delete, Param } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiOkResponse, ApiHeader } from '@nestjs/swagger';
import { Controller, Get, UseGuards, Patch, Body} from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiOkResponse } from '@nestjs/swagger';
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
import { UserService } from '../providers/user.service';
import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto';
import { CreateUserAddressDto } from '../dto/create-user-address.dto';
import { UpdateUserAddressDto } from '../dto/update-user-address.dto';
import { UserId } from 'src/common/decorators/user-id.decorator';
import { RestId } from 'src/common/decorators';
import { } from 'src/common/decorators';
import { FindUsersDto } from '../dto/find-user.dto';
import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto';
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
import { API_HEADER_SLUG } from 'src/common/constants/index';
import { UserSuccessMessage } from 'src/common/enums/message.enum';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { Permission } from 'src/common/enums/permission.enum';
@@ -40,7 +40,7 @@ export class UsersController {
@ApiBody({ type: UpdateUserDto })
@Patch('public/user/update')
async updateUser(@UserId() userId: string, , @Body() dto: UpdateUserDto) {
const user = await this.userService.updateUser(userId, restId, dto);
const user = await this.userService.updateUser(userId, , dto);
return user;
}
@@ -55,129 +55,128 @@ export class UsersController {
return addresses;
}
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get User Wallet' })
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @ApiOperation({ summary: 'Get User Wallet' })
@Get('public/user/wallet/balance')
async getUserWalletBalance(@UserId() userId: string,) {
const wallet = await this.walletService.getUserCurrentWalletBalance(userId, restId);
return wallet;
}
// @Get('public/user/wallet/balance')
// async getUserWalletBalance(@UserId() userId: string,) {
// const wallet = await this.walletService.getUserCurrentWalletBalance(userId,);
// return wallet;
// }
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get User Wallet' })
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @ApiOperation({ summary: 'Get User Wallet' })
// @Get('public/user/points/balance')
// async getUserWallet(@UserId() userId: string,) {
// const wallet = await this.walletService.getUserCurrentPoinrBalance(userId,);
// return wallet;
// }
@Get('public/user/points/balance')
async getUserWallet(@UserId() userId: string,) {
const wallet = await this.walletService.getUserCurrentPoinrBalance(userId, restId);
return wallet;
}
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @ApiOperation({ summary: 'Get User Wallet Transactions' })
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get User Wallet Transactions' })
// @Get('public/user/wallet/transactions')
// async getUserWalletTransactions(
// @UserId() userId: string,
// ,
// @Query(new ValidationPipe({ transform: true, whitelist: true }))
// query: FindWalletTransactionsDto,
// ) {
// const transactions = await this.userService.getUserWalletTransactions(userId, , query);
// return transactions;
// }
@Get('public/user/wallet/transactions')
async getUserWalletTransactions(
@UserId() userId: string,
,
@Query(new ValidationPipe({ transform: true, whitelist: true }))
query: FindWalletTransactionsDto,
) {
const transactions = await this.userService.getUserWalletTransactions(userId, restId, query);
return transactions;
}
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @ApiOperation({ summary: 'Create a new address for the authenticated user' })
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Create a new address for the authenticated user' })
// @ApiBody({ type: CreateUserAddressDto })
// @ApiOkResponse({ description: 'Created user address' })
// @Post('public/user/addresses')
// async createUserAddress(
// @UserId() userId: string,
// @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: CreateUserAddressDto,
// ) {
// const address = await this.userService.createUserAddress(userId, dto);
// return address;
// }
@ApiBody({ type: CreateUserAddressDto })
@ApiOkResponse({ description: 'Created user address' })
@Post('public/user/addresses')
async createUserAddress(
@UserId() userId: string,
@Body(new ValidationPipe({ transform: true, whitelist: true })) dto: CreateUserAddressDto,
) {
const address = await this.userService.createUserAddress(userId, dto);
return address;
}
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @ApiOperation({ summary: 'Get a single address by ID for the authenticated user' })
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get a single address by ID for the authenticated user' })
// @ApiOkResponse({ description: 'User address details' })
// @Get('public/user/addresses/:addressId')
// async getUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) {
// const address = await this.userService.getUserAddress(userId, addressId);
// return address;
// }
@ApiOkResponse({ description: 'User address details' })
@Get('public/user/addresses/:addressId')
async getUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) {
const address = await this.userService.getUserAddress(userId, addressId);
return address;
}
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @ApiOperation({ summary: 'Update an address for the authenticated user' })
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Update an address for the authenticated user' })
// @ApiBody({ type: UpdateUserAddressDto })
// @ApiOkResponse({ description: 'Updated user address' })
// @Patch('public/user/addresses/:addressId')
// async updateUserAddress(
// @UserId() userId: string,
// @Param('addressId') addressId: string,
// @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserAddressDto,
// ) {
// const address = await this.userService.updateUserAddress(userId, addressId, dto);
// return address;
// }
@ApiBody({ type: UpdateUserAddressDto })
@ApiOkResponse({ description: 'Updated user address' })
@Patch('public/user/addresses/:addressId')
async updateUserAddress(
@UserId() userId: string,
@Param('addressId') addressId: string,
@Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserAddressDto,
) {
const address = await this.userService.updateUserAddress(userId, addressId, dto);
return address;
}
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @ApiOperation({ summary: 'Delete an address for the authenticated user' })
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Delete an address for the authenticated user' })
// @ApiOkResponse({ description: 'Address deleted successfully' })
// @Delete('public/user/addresses/:addressId')
// async deleteUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) {
// await this.userService.deleteUserAddress(userId, addressId);
// return { message: UserSuccessMessage.ADDRESS_DELETED_SUCCESS };
// }
@ApiOkResponse({ description: 'Address deleted successfully' })
@Delete('public/user/addresses/:addressId')
async deleteUserAddress(@UserId() userId: string, @Param('addressId') addressId: string) {
await this.userService.deleteUserAddress(userId, addressId);
return { message: UserSuccessMessage.ADDRESS_DELETED_SUCCESS };
}
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @ApiOperation({ summary: 'Set an address as default for the authenticated user' })
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Set an address as default for the authenticated user' })
// @ApiOkResponse({ description: 'Address set as default' })
// @Patch('public/user/addresses/:addressId/default')
// async setDefaultAddress(@UserId() userId: string, @Param('addressId') addressId: string) {
// const address = await this.userService.setDefaultAddress(userId, addressId);
// return address;
// }
@ApiOkResponse({ description: 'Address set as default' })
@Patch('public/user/addresses/:addressId/default')
async setDefaultAddress(@UserId() userId: string, @Param('addressId') addressId: string) {
const address = await this.userService.setDefaultAddress(userId, addressId);
return address;
}
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @ApiOperation({ summary: 'Convert user score (points) to wallet balance' })
@UseGuards(AuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Convert user score (points) to wallet balance' })
// @Post('public/user/convert-score-to-wallet')
// async convertScoreToWallet(@UserId() userId: string,) {
// await this.userService.convertScoreToWallet(userId,);
// return {
// message: UserSuccessMessage.SCORE_CONVERTED_SUCCESS,
// };
// }
@Post('public/user/convert-score-to-wallet')
async convertScoreToWallet(@UserId() userId: string,) {
await this.userService.convertScoreToWallet(userId, restId);
return {
message: UserSuccessMessage.SCORE_CONVERTED_SUCCESS,
};
}
// /******************** Admin Routes **********************/
/******************** Admin Routes **********************/
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_USERS)
@ApiOperation({ summary: 'Get paginated list of restuarant users with filters' })
@Get('admin/users')
async findAllRestaurantUsers(
@Query(new ValidationPipe({ transform: true, whitelist: true }))
query: FindUsersDto,
,
) {
return this.userService.findAll(restId, query);
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Permissions(Permission.MANAGE_USERS)
// @ApiOperation({ summary: 'Get paginated list of restuarant users with filters' })
// @Get('admin/users')
// async findAllRestaurantUsers(
// @Query(new ValidationPipe({ transform: true, whitelist: true }))
// query: FindUsersDto,
// ,
// ) {
// return this.userService.findAll(, query);
// }
}
+29
View File
@@ -0,0 +1,29 @@
import { IsString, IsBoolean, IsNotEmpty, IsMobilePhone, IsInt } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
@IsMobilePhone('fa-IR')
phone: string;
@ApiProperty({ description: "User's first name", example: 'علی' })
@IsString()
@IsNotEmpty()
firstName: string;
@ApiProperty({ description: "User's last name", example: 'جعفری' })
@IsString()
@IsNotEmpty()
lastName: string;
@ApiProperty({ description: 'Gender flag (boolean)', example: true })
@IsBoolean()
gender: boolean;
@ApiProperty({ description: "User's max credit in Toman", example: 100000 })
@IsInt()
maxCredit: number;
}
@@ -1,34 +0,0 @@
import { IsNotEmpty, IsNumber, IsIn } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
import { WalletTransactionType, WalletTransactionReason } from '../interface/wallet';
export class CreateWalletTransactionDto {
@ApiProperty({
description: 'Transaction amount',
type: Number,
example: 1000,
})
@IsNotEmpty()
@IsNumber()
@Type(() => Number)
amount!: number;
@ApiProperty({
description: 'Transaction type',
enum: WalletTransactionType,
example: WalletTransactionType.CREDIT,
})
@IsNotEmpty()
@IsIn(Object.values(WalletTransactionType))
type!: WalletTransactionType;
@ApiProperty({
description: 'Transaction reason',
enum: WalletTransactionReason,
example: WalletTransactionReason.ORDER_PAYMENT,
})
@IsNotEmpty()
@IsIn(Object.values(WalletTransactionReason))
reason!: WalletTransactionReason;
}
+3 -34
View File
@@ -1,35 +1,4 @@
import { IsString, IsOptional, IsBoolean } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { PartialType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto {
@ApiPropertyOptional({ description: "User's first name", example: 'John' })
@IsOptional()
@IsString()
firstName?: string;
@ApiPropertyOptional({ description: "User's last name", example: 'Doe' })
@IsOptional()
@IsString()
lastName?: string;
@ApiPropertyOptional({ description: "User's birth date (ISO)", example: '1990-01-01' })
@IsOptional()
// keep as string here, caller should send ISO date; service will assign to Date
@IsString()
birthDate?: string;
@ApiPropertyOptional({ description: "User's marriage date (ISO)", example: '2015-06-01' })
@IsOptional()
@IsString()
marriageDate?: string;
@ApiPropertyOptional({ description: 'Gender flag (boolean)', example: true })
@IsOptional()
@IsBoolean()
gender?: boolean;
@ApiPropertyOptional({ description: 'Avatar URL' })
@IsOptional()
@IsString()
avatarUrl?: string;
}
export class UpdateUserDto extends PartialType(CreateUserDto) {}
@@ -0,0 +1,24 @@
import { Entity, Property, ManyToOne, Index, Enum } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { User } from './user.entity';
import { CreditTransactionType } from '../interface/credit';
@Entity({ tableName: 'credit_transactions' })
@Index({ properties: ['user'] })
export class CreditTransaction extends BaseEntity {
@ManyToOne(() => User)
user!: User;
@Property()
orderId: string
@Property({ type: 'decimal', precision: 10, scale: 0 })
amount!: number;
@Property({ type: 'decimal', precision: 10, scale: 0 })
balance!: number;
@Enum(() => CreditTransaction)
type!: CreditTransactionType;
}
@@ -1,30 +0,0 @@
import { Entity, Property, ManyToOne, Index, Enum } 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';
import { PointTransactionReason } from '../interface/point';
import { PointTransactionType } from '../interface/point';
@Entity({ tableName: 'point_transactions' })
@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 PointTransaction extends BaseEntity {
@ManyToOne(() => User)
user!: User;
@ManyToOne(() => User)
restaurant!: Restaurant;
@Property({ type: 'decimal', precision: 10, scale: 0 })
amount!: number;
@Property({ type: 'decimal', precision: 10, scale: 0 })
balance!: number;
@Enum(() => PointTransaction)
type!: PointTransactionType;
@Enum(() => PointTransactionReason)
reason!: PointTransactionReason;
}
+8 -12
View File
@@ -1,15 +1,18 @@
import { Entity, Index, Property, OneToMany, Collection, Cascade } from '@mikro-orm/core';
import { Entity, Index, Property, OneToMany, Collection, Cascade, PrimaryKey } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { UserAddress } from './user-address.entity';
import { Order } from 'src/modules/order/entities/order.entity';
import { normalizePhone } from '../../util/phone.util';
import { ulid } from 'ulid';
@Entity({ tableName: 'users' })
@Index({ properties: ['isActive'] })
export class User extends BaseEntity {
@OneToMany(() => Order, order => order.user)
orders = new Collection<Order>(this);
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
id: string = ulid();
@Property()
firstName!: string;
@@ -27,14 +30,6 @@ export class User extends BaseEntity {
this._phone = normalizePhone(value);
}
@Property({ default: null, type: 'date', nullable: true })
birthDate?: Date;
@Property({ default: null, type: 'date', nullable: true })
marriageDate?: Date;
@Property({ nullable: true })
referrer?: string;
@Property({ default: true })
isActive?: boolean = true;
@@ -42,8 +37,9 @@ export class User extends BaseEntity {
@Property({ default: true, nullable: true })
gender?: boolean;
@Property({ nullable: true })
avatarUrl?: string;
@Property({ default: 0 })
maxCredit: number;
@OneToMany(() => UserAddress, address => address.user, {
cascade: [Cascade.ALL],
@@ -1,29 +0,0 @@
import { Entity, Index, Property, ManyToOne, Enum } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { User } from './user.entity';
import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet';
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
@Entity({ tableName: 'wallet_transactions' })
@Index({ properties: ['user', 'restaurant'] })
@Index({ properties: ['user'] })
@Index({ properties: ['restaurant'] })
export class WalletTransaction extends BaseEntity {
@ManyToOne(() => User)
user!: User;
@ManyToOne(() => Restaurant)
restaurant!: Restaurant;
@Property({ type: 'decimal', precision: 10, scale: 0 })
amount!: number;
@Property({ type: 'decimal', precision: 10, scale: 0 })
balance!: number;
@Enum(() => WalletTransactionType)
type!: WalletTransactionType;
@Enum(() => WalletTransactionReason)
reason!: WalletTransactionReason;
}
+6
View File
@@ -0,0 +1,6 @@
export enum CreditTransactionType {
WITHDRAW = 'withdraw',
DEPOSIT = 'deposit',
}
-10
View File
@@ -1,10 +0,0 @@
export enum PointTransactionType {
CREDIT = 'credit',
DEBIT = 'debit',
}
export enum PointTransactionReason {
ORDER_COMPLETED_DEPOSIT = 'order_completed_deposit',
CONVERT_SCORE_TO_POINT = 'convert_score_to_point',
}
-13
View File
@@ -1,13 +0,0 @@
export enum WalletTransactionType {
CREDIT = 'credit',
DEBIT = 'debit',
}
export enum WalletTransactionReason {
ORDER_PAYMENT = 'order_payment',
ORDER_REFUND = 'order_refund',
CONVERT_SCORE_TO_WALLET = 'convert_score_to_wallet',
WITHDRAW = 'withdraw',
DEPOSIT = 'deposit',
}
+124 -309
View File
@@ -1,71 +1,46 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { FilterQuery, RequiredEntityData } from '@mikro-orm/core';
import { RequiredEntityData } from '@mikro-orm/core';
import { User } from '../entities/user.entity';
import { UserAddress } from '../entities/user-address.entity';
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
import { EntityManager } from '@mikro-orm/postgresql';
import { UpdateUserDto } from '../dto/update-user.dto';
import { CreateUserDto } from '../dto/create-user.dto';
import { FindUsersDto } from '../dto/find-user.dto';
import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto';
import { CreateWalletTransactionDto } from '../dto/create-wallet-transaction.dto';
import { CreateUserAddressDto } from '../dto/create-user-address.dto';
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 '../../util/phone.util';
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
import { WalletTransactionRepository } from '../repositories/wallet-transaction.repository';
import { WalletService } from './wallet.service';
import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet';
import { WalletTransaction } from '../entities/wallet-transaction.entity';
import { UpdateUserDto } from '../dto/update-user.dto';
@Injectable()
export class UserService {
constructor(
private readonly userRepository: UserRepository,
private readonly restaurantRepository: RestRepository,
private readonly walletTransactionRepository: WalletTransactionRepository,
private readonly walletService: WalletService,
private readonly em: EntityManager,
) { }
async findOrCreateByPhone(phone: string): Promise<User> {
const normalizedPhone = normalizePhone(phone);
let user = await this.userRepository.findOne({ phone: normalizedPhone });
async create(dto: CreateUserDto): Promise<User> {
const normalizedPhone = normalizePhone(dto.phone)
if (!user) {
// firstName is required on the entity; use phone as a sensible default
const _raw = {
phone: normalizedPhone,
firstName: '[نام]',
// keep dates undefined (no value) — cast through unknown to satisfy TS typing
birthDate: undefined,
marriageDate: undefined,
wallet: 0,
points: 0,
};
const currentUser = await this.userRepository.findOne({ phone: normalizedPhone });
const createData = _raw as unknown as RequiredEntityData<User>;
user = this.userRepository.create(createData);
await this.em.persistAndFlush(user);
if (currentUser) {
return currentUser
}
const createData: RequiredEntityData<User> = {
phone: normalizedPhone,
firstName: dto.firstName,
lastName: dto.lastName,
gender: dto.gender,
maxCredit: dto.maxCredit
};
const user = this.userRepository.create(createData);
await this.em.persistAndFlush([user]);
return user;
}
// async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
// const user = await this.em.findOneOrFail(User, userId, {}).catch(() => {
// throw new NotFoundException(`User with ID ${userId} not found.`);
// });
// this.em.assign(user, { ...dto });
// await this.em.flush();
// return user;
// }
async findByPhone(phone: string): Promise<User | null> {
const normalizedPhone = normalizePhone(phone);
return this.userRepository.findOne({ phone: normalizedPhone });
@@ -74,107 +49,24 @@ export class UserService {
async findById(id: string): Promise<User | null> {
return this.userRepository.findOne({ id });
}
// todo : transaction code here
async create(phone: string, restId: string): Promise<User> {
const normalizedPhone = normalizePhone(phone);
const _raw = {
phone: normalizedPhone,
firstName: normalizedPhone,
birthDate: undefined,
marriageDate: undefined,
};
// get registerscore from restaurant
const restaurant = await this.restaurantRepository.findOne({ id: restId });
if (!restaurant) {
throw new NotFoundException(`Restaurant not found.`);
}
const points = Number(restaurant.score?.registerScore || 0);
const createData = _raw as unknown as RequiredEntityData<User>;
const user = this.userRepository.create(createData);
const newWalletTransaction = this.walletTransactionRepository.create({
user: user,
restaurant: restaurant,
balance: 0,
amount: points,
type: WalletTransactionType.CREDIT,
reason: WalletTransactionReason.DEPOSIT
});
await this.em.persistAndFlush([user, newWalletTransaction]);
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.`);
}
// Normalize date strings into Date objects if provided
const assignData: Partial<User> = { ...dto } as Partial<User>;
if (dto.birthDate) {
assignData.birthDate = new Date(dto.birthDate);
}
if (dto.marriageDate) {
assignData.marriageDate = new Date(dto.marriageDate);
}
// get restuarant
this.em.assign(user, assignData);
this.em.assign(user, dto);
await this.em.flush();
return user;
}
async findAll(restId: string, dto: FindUsersDto): Promise<PaginatedResult<User>> {
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto;
// 1. Calculate pagination
const offset = (page - 1) * limit;
// 2. Build the 'where' filter query
const where: FilterQuery<User> = {
orders: {
restaurant: {
id: restId,
},
},
};
// 4. Add 'search' logic (case-insensitive)
if (search) {
const searchPattern = `%${search}%`;
const normalizedSearch = normalizePhone(search);
const normalizedSearchPattern = `%${normalizedSearch}%`;
// $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default)
// Search with both original and normalized phone patterns to handle various input formats
where.$or = [
{ firstName: { $ilike: searchPattern } },
{ lastName: { $ilike: searchPattern } },
{ phone: { $ilike: searchPattern } },
...(normalizedSearch !== search ? [{ phone: { $ilike: normalizedSearchPattern } }] : []),
];
}
// 5. Execute the query using findAndCount
const [users, total] = await this.userRepository.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
});
// 6. Calculate total pages
const totalPages = Math.ceil(total / limit);
// 7. Return the paginated result
return {
data: users,
meta: {
total,
page,
limit,
totalPages,
},
};
async findAll(dto: FindUsersDto): Promise<PaginatedResult<User>> {
return this.userRepository.findAllPaginated(dto)
}
async getUserAddresses(userId: string): Promise<UserAddress[]> {
@@ -188,201 +80,124 @@ export class UserService {
return user.addresses.getItems();
}
async createUserAddress(userId: string, dto: CreateUserAddressDto): Promise<UserAddress> {
const user = await this.userRepository.findOne({ id: userId });
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found.`);
}
// async createUserAddress(userId: string, dto: CreateUserAddressDto): Promise<UserAddress> {
// const user = await this.userRepository.findOne({ id: userId });
// if (!user) {
// throw new NotFoundException(`User with ID ${userId} not found.`);
// }
// If setting as default, unset other default addresses
if (dto.isDefault) {
const existingAddresses = await this.em.find(UserAddress, { user: { id: userId }, isDefault: true });
for (const address of existingAddresses) {
address.isDefault = false;
}
await this.em.flush();
}
// // If setting as default, unset other default addresses
// if (dto.isDefault) {
// const existingAddresses = await this.em.find(UserAddress, { user: { id: userId }, isDefault: true });
// for (const address of existingAddresses) {
// address.isDefault = false;
// }
// await this.em.flush();
// }
// Create new address
const address = this.em.create(UserAddress, {
user,
title: dto.title,
address: dto.address,
city: dto.city,
province: dto.province,
postalCode: dto.postalCode,
latitude: dto.latitude,
longitude: dto.longitude,
phone: dto.phone,
isDefault: dto.isDefault ?? false,
});
// // Create new address
// const address = this.em.create(UserAddress, {
// user,
// title: dto.title,
// address: dto.address,
// city: dto.city,
// province: dto.province,
// postalCode: dto.postalCode,
// latitude: dto.latitude,
// longitude: dto.longitude,
// phone: dto.phone,
// isDefault: dto.isDefault ?? false,
// });
await this.em.persistAndFlush(address);
return address;
}
// await this.em.persistAndFlush(address);
// return address;
// }
async getUserAddress(userId: string, addressId: string): Promise<UserAddress> {
const address = await this.em.findOne(UserAddress, {
id: addressId,
user: { id: userId },
});
// async getUserAddress(userId: string, addressId: string): Promise<UserAddress> {
// const address = await this.em.findOne(UserAddress, {
// id: addressId,
// user: { id: userId },
// });
if (!address) {
throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
}
// if (!address) {
// throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
// }
return address;
}
// return address;
// }
async updateUserAddress(userId: string, addressId: string, dto: UpdateUserAddressDto): Promise<UserAddress> {
const address = await this.em.findOne(UserAddress, {
id: addressId,
user: { id: userId },
});
// async updateUserAddress(userId: string, addressId: string, dto: UpdateUserAddressDto): Promise<UserAddress> {
// const address = await this.em.findOne(UserAddress, {
// id: addressId,
// user: { id: userId },
// });
if (!address) {
throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
}
// if (!address) {
// throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
// }
// If setting as default, unset other default addresses
if (dto.isDefault === true) {
const existingAddresses = await this.em.find(UserAddress, {
user: { id: userId },
isDefault: true,
id: { $ne: addressId },
});
for (const existingAddress of existingAddresses) {
existingAddress.isDefault = false;
}
await this.em.flush();
}
// // If setting as default, unset other default addresses
// if (dto.isDefault === true) {
// const existingAddresses = await this.em.find(UserAddress, {
// user: { id: userId },
// isDefault: true,
// id: { $ne: addressId },
// });
// for (const existingAddress of existingAddresses) {
// existingAddress.isDefault = false;
// }
// await this.em.flush();
// }
// Update address fields
this.em.assign(address, dto);
await this.em.flush();
// // Update address fields
// this.em.assign(address, dto);
// await this.em.flush();
return address;
}
// return address;
// }
async deleteUserAddress(userId: string, addressId: string): Promise<void> {
const address = await this.em.findOne(UserAddress, {
id: addressId,
user: { id: userId },
});
// async deleteUserAddress(userId: string, addressId: string): Promise<void> {
// const address = await this.em.findOne(UserAddress, {
// id: addressId,
// user: { id: userId },
// });
if (!address) {
throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
}
// if (!address) {
// throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
// }
// Soft delete the address
address.deletedAt = new Date();
await this.em.persistAndFlush(address);
}
// // Soft delete the address
// address.deletedAt = new Date();
// await this.em.persistAndFlush(address);
// }
async setDefaultAddress(userId: string, addressId: string): Promise<UserAddress> {
const address = await this.em.findOne(UserAddress, {
id: addressId,
user: { id: userId },
});
// async setDefaultAddress(userId: string, addressId: string): Promise<UserAddress> {
// const address = await this.em.findOne(UserAddress, {
// id: addressId,
// user: { id: userId },
// });
if (!address) {
throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
}
// if (!address) {
// throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
// }
// Unset all other default addresses
const existingAddresses = await this.em.find(UserAddress, {
user: { id: userId },
isDefault: true,
id: { $ne: addressId },
});
for (const existingAddress of existingAddresses) {
existingAddress.isDefault = false;
}
// // Unset all other default addresses
// const existingAddresses = await this.em.find(UserAddress, {
// user: { id: userId },
// isDefault: true,
// id: { $ne: addressId },
// });
// for (const existingAddress of existingAddresses) {
// existingAddress.isDefault = false;
// }
// Set this address as default
address.isDefault = true;
await this.em.flush();
// // Set this address as default
// address.isDefault = true;
// await this.em.flush();
return address;
}
// return address;
// }
async convertScoreToWallet(userId: string, restId: string) {
return this.em.transactional(async em => {
const user = await em.findOne(User, { id: userId });
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found.`);
}
const restaurant = await em.findOne(Restaurant, { id: restId });
if (!restaurant) {
throw new NotFoundException(`Restaurant with ID ${restId} not found.`);
}
let walletTransaction = await em.findOne(WalletTransaction, { user: { id: userId }, restaurant: { id: restId } },
{ orderBy: { createdAt: 'DESC' } });
if (!walletTransaction) {
walletTransaction = em.create(WalletTransaction,
{
user: user,
restaurant,
balance: 0,
amount: 0,
type: WalletTransactionType.CREDIT,
reason: WalletTransactionReason.DEPOSIT
});
await em.persistAndFlush(walletTransaction);
}
// Determine how many points to convert
const pointsToConvert = walletTransaction.balance;
// Validate points to convert
if (pointsToConvert <= 0) {
throw new BadRequestException('Points to convert must be greater than 0.');
}
if (!restaurant.score) {
throw new BadRequestException('Restaurant score not found.');
}
if (!restaurant.score.purchaseScore) {
throw new BadRequestException('Restaurant purchase score not found.');
}
if (!restaurant.score.purchaseAmount) {
throw new BadRequestException('Restaurant purchase amount not found.');
}
const walletIncreaseAmount =
(Number(pointsToConvert) * Number(restaurant.score.purchaseAmount)) / Number(restaurant.score.purchaseScore);
// Convert points to wallet (1:1 ratio - can be made configurable)
const newWalletTransaction = em.create(WalletTransaction, {
user: user,
restaurant: restaurant,
amount: walletIncreaseAmount,
type: WalletTransactionType.CREDIT,
reason: WalletTransactionReason.CONVERT_SCORE_TO_WALLET,
balance: walletTransaction.balance + walletIncreaseAmount,
});
// Update user's wallet and points
walletTransaction.balance = walletTransaction.balance + walletIncreaseAmount;
em.persist([newWalletTransaction]);
await em.flush();
return user;
});
}
getUserWallet(userId: string, restId: string): Promise<WalletTransaction | null> {
return this.walletTransactionRepository.findOne({ user: { id: userId }, restaurant: { id: restId } },
{ orderBy: { createdAt: 'DESC' } });
}
getUserWalletTransactions(userId: string, restId: string, dto: FindWalletTransactionsDto) {
return this.walletService.getUserWalletTransactions(userId, restId, dto);
}
async createWalletTransaction(userId: string, restId: string, dto: CreateWalletTransactionDto) {
return this.em.transactional(async em => {
return this.walletService.createTransaction(em, userId, restId, dto);
});
}
}
+148 -151
View File
@@ -1,183 +1,180 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { FilterQuery } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/postgresql';
import { WalletTransaction } from '../entities/wallet-transaction.entity';
import { WalletTransactionRepository } from '../repositories/wallet-transaction.repository';
import { CreditTransactionRepository } from '../repositories/credit-transaction.repository';
import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto';
import { CreateWalletTransactionDto } from '../dto/create-wallet-transaction.dto';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { WalletTransactionType } from '../interface/wallet';
import { PointTransactionRepository } from '../repositories/point-transaction.repository';
import { CreditTransaction } from '../entities/credit-transaction.entity';
@Injectable()
export class WalletService {
constructor(
private readonly walletTransactionRepository: WalletTransactionRepository,
private readonly pointTransactionRepository: PointTransactionRepository,
) {}
private readonly walletTransactionRepository: CreditTransactionRepository,
private readonly pointTransactionRepository: CreditTransaction,
) { }
async getUserWalletTransactions(
userId: string,
restId: string,
dto: FindWalletTransactionsDto,
): Promise<PaginatedResult<WalletTransaction>> {
const {
page = 1,
limit = 10,
type,
reason,
startDate,
endDate,
minAmount,
maxAmount,
orderBy = 'createdAt',
order = 'desc',
} = dto;
// async getUserWalletTransactions(
// userId: string,
// dto: FindWalletTransactionsDto,
// ): Promise<PaginatedResult<CreditTransaction>> {
// const {
// page = 1,
// limit = 10,
// type,
// reason,
// startDate,
// endDate,
// minAmount,
// maxAmount,
// orderBy = 'createdAt',
// order = 'desc',
// } = dto;
// Calculate pagination
const offset = (page - 1) * limit;
// // Calculate pagination
// const offset = (page - 1) * limit;
// Find the user's wallet for this restaurant
const walletTransaction = await this.walletTransactionRepository.findOne({
user: { id: userId },
restaurant: { id: restId },
});
// // Find the user's wallet for this restaurant
// const walletTransaction = await this.walletTransactionRepository.findOne({
// user: { id: userId },
// restaurant: { id: },
// });
if (!walletTransaction) {
throw new NotFoundException(`User wallet not found for user ${userId} and restaurant ${restId}.`);
}
// if (!walletTransaction) {
// throw new NotFoundException(`User wallet not found for user ${userId} and restaurant ${}.`);
// }
// Build the 'where' filter query
const where: FilterQuery<WalletTransaction> = {
user: { id: userId },
restaurant: { id: restId },
};
// // Build the 'where' filter query
// const where: FilterQuery<WalletTransaction> = {
// user: { id: userId },
// restaurant: { id: },
// };
// Add type filter
if (type) {
where.type = type;
}
// // Add type filter
// if (type) {
// where.type = type;
// }
// Add reason filter
if (reason) {
where.reason = reason;
}
// // Add reason filter
// if (reason) {
// where.reason = reason;
// }
// Add date range filters
if (startDate || endDate) {
where.createdAt = {};
if (startDate) {
where.createdAt.$gte = new Date(startDate);
}
if (endDate) {
where.createdAt.$lte = new Date(endDate);
}
}
// // Add date range filters
// if (startDate || endDate) {
// where.createdAt = {};
// if (startDate) {
// where.createdAt.$gte = new Date(startDate);
// }
// if (endDate) {
// where.createdAt.$lte = new Date(endDate);
// }
// }
// Add amount range filters
if (minAmount !== undefined || maxAmount !== undefined) {
where.amount = {};
if (minAmount !== undefined) {
where.amount.$gte = minAmount;
}
if (maxAmount !== undefined) {
where.amount.$lte = maxAmount;
}
}
// // Add amount range filters
// if (minAmount !== undefined || maxAmount !== undefined) {
// where.amount = {};
// if (minAmount !== undefined) {
// where.amount.$gte = minAmount;
// }
// if (maxAmount !== undefined) {
// where.amount.$lte = maxAmount;
// }
// }
// Execute the query using findAndCount
const [transactions, total] = await this.walletTransactionRepository.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
});
// // Execute the query using findAndCount
// const [transactions, total] = await this.walletTransactionRepository.findAndCount(where, {
// limit,
// offset,
// orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
// });
// Calculate total pages
const totalPages = Math.ceil(total / limit);
// // Calculate total pages
// const totalPages = Math.ceil(total / limit);
// Return the paginated result
return {
data: transactions,
meta: {
total,
page,
limit,
totalPages,
},
};
}
// // Return the paginated result
// return {
// data: transactions,
// meta: {
// total,
// page,
// limit,
// totalPages,
// },
// };
// }
getUserWallet(userId: string, restId: string): Promise<WalletTransaction | null> {
return this.walletTransactionRepository.findOne(
{
user: { id: userId },
restaurant: { id: restId },
},
{ orderBy: { createdAt: 'DESC' } },
);
}
// getUserWallet(userId: string, : string): Promise<WalletTransaction | null> {
// return this.walletTransactionRepository.findOne(
// {
// user: { id: userId },
// restaurant: { id: },
// },
// { orderBy: { createdAt: 'DESC' } },
// );
// }
async createTransaction(
em: EntityManager,
userId: string,
restId: string,
dto: CreateWalletTransactionDto,
): Promise<WalletTransaction> {
const { amount, type, reason } = dto;
// async createTransaction(
// em: EntityManager,
// userId: string,
// : string,
// dto: CreateWalletTransactionDto,
// ): Promise<WalletTransaction> {
// const { amount, type, reason } = dto;
// Validate amount
if (amount <= 0) {
throw new BadRequestException('Transaction amount must be greater than 0.');
}
// // Validate amount
// if (amount <= 0) {
// throw new BadRequestException('Transaction amount must be greater than 0.');
// }
// Find the user's wallet for this restaurant
const walletTransaction = await em.findOne(WalletTransaction, {
user: { id: userId },
restaurant: { id: restId },
});
// // Find the user's wallet for this restaurant
// const walletTransaction = await em.findOne(WalletTransaction, {
// user: { id: userId },
// restaurant: { id: },
// });
if (!walletTransaction) {
throw new NotFoundException(`User wallet not found for user ${userId} and restaurant ${restId}.`);
}
// if (!walletTransaction) {
// throw new NotFoundException(`User wallet not found for user ${userId} and restaurant ${}.`);
// }
// Calculate new balance
const currentBalance = walletTransaction.balance || 0;
let newBalance: number;
// // Calculate new balance
// const currentBalance = walletTransaction.balance || 0;
// let newBalance: number;
if (type === WalletTransactionType.CREDIT) {
newBalance = currentBalance + amount;
} else if (type === WalletTransactionType.DEBIT) {
if (currentBalance < amount) {
throw new BadRequestException(`Insufficient wallet balance. Current: ${currentBalance}, Required: ${amount}.`);
}
newBalance = currentBalance - amount;
} else {
throw new BadRequestException(`Invalid transaction type: ${type}`);
}
// if (type === WalletTransactionType.CREDIT) {
// newBalance = currentBalance + amount;
// } else if (type === WalletTransactionType.DEBIT) {
// if (currentBalance < amount) {
// throw new BadRequestException(`Insufficient wallet balance. Current: ${currentBalance}, Required: ${amount}.`);
// }
// newBalance = currentBalance - amount;
// } else {
// throw new BadRequestException(`Invalid transaction type: ${type}`);
// }
// Update wallet balance
walletTransaction.balance = newBalance;
await em.persistAndFlush(walletTransaction);
// // Update wallet balance
// walletTransaction.balance = newBalance;
// await em.persistAndFlush(walletTransaction);
// Create transaction record
const transaction = em.create(WalletTransaction, {
user: walletTransaction.user,
restaurant: walletTransaction.restaurant,
amount,
type,
reason,
balance: newBalance,
});
await em.persistAndFlush([walletTransaction, transaction]);
return transaction;
}
// // Create transaction record
// const transaction = em.create(WalletTransaction, {
// user: walletTransaction.user,
// restaurant: walletTransaction.restaurant,
// amount,
// type,
// reason,
// balance: newBalance,
// });
// await em.persistAndFlush([walletTransaction, transaction]);
// return transaction;
// }
async getUserCurrentWalletBalance(userId: string, restuarantId: string) {
const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, restuarantId);
return { balance };
}
async getUserCurrentPoinrBalance(userId: string, restuarantId: string) {
const balance = await this.pointTransactionRepository.getcurrentPointBalance(userId, restuarantId);
return { balance };
}
// async getUserCurrentWalletBalance(userId: string, restuarantId: string) {
// const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, restuarantId);
// return { balance };
// }
// async getUserCurrentPoinrBalance(userId: string, restuarantId: string) {
// const balance = await this.pointTransactionRepository.getcurrentPointBalance(userId, restuarantId);
// return { balance };
// }
}
@@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { CreditTransaction } from '../entities/credit-transaction.entity';
@Injectable()
export class CreditTransactionRepository extends EntityRepository<CreditTransaction> {
constructor(readonly em: EntityManager) {
super(em, CreditTransaction);
}
async getCurrentCredit(userId: string,): Promise<number> {
const lastRow = await this.em.findOne(
CreditTransaction,
{
user: { id: userId },
},
{ orderBy: { createdAt: 'desc' } },
);
return lastRow?.balance || 0;
}
}
@@ -1,17 +0,0 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { PointTransaction } from '../entities/point-transaction.entity';
@Injectable()
export class PointTransactionRepository extends EntityRepository<PointTransaction> {
constructor(readonly em: EntityManager) {
super(em, PointTransaction);
}
async getcurrentPointBalance(userId: string, restaurantId: string): Promise<number> {
const pointTransaction = await this.em.findOne(PointTransaction, {
user: { id: userId },
restaurant: { id: restaurantId },
});
return pointTransaction?.balance || 0;
}
}
@@ -1,10 +1,59 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { EntityManager, EntityRepository, FilterQuery } from '@mikro-orm/postgresql';
import { User } from '../entities/user.entity';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { FindUsersDto } from '../dto/find-user.dto';
import { normalizePhone } from 'src/modules/util/phone.util';
@Injectable()
export class UserRepository extends EntityRepository<User> {
constructor(readonly em: EntityManager) {
super(em, User);
}
async findAllPaginated(dto: FindUsersDto): Promise<PaginatedResult<User>> {
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto;
// 1. Calculate pagination
const offset = (page - 1) * limit;
// 2. Build the 'where' filter query
const where: FilterQuery<User> = {};
// 4. Add 'search' logic (case-insensitive)
if (search) {
const searchPattern = `%${search}%`;
const normalizedSearch = normalizePhone(search);
const normalizedSearchPattern = `%${normalizedSearch}%`;
// $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default)
// Search with both original and normalized phone patterns to handle various input formats
where.$or = [
{ firstName: { $ilike: searchPattern } },
{ lastName: { $ilike: searchPattern } },
{ phone: { $ilike: searchPattern } },
...(normalizedSearch !== search ? [{ phone: { $ilike: normalizedSearchPattern } }] : []),
];
}
// 5. Execute the query using findAndCount
const [users, total] = await this.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
});
// 6. Calculate total pages
const totalPages = Math.ceil(total / limit);
// 7. Return the paginated result
return {
data: users,
meta: {
total,
page,
limit,
totalPages,
},
};
}
}
@@ -1,21 +0,0 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { WalletTransaction } from '../entities/wallet-transaction.entity';
@Injectable()
export class WalletTransactionRepository extends EntityRepository<WalletTransaction> {
constructor(readonly em: EntityManager) {
super(em, WalletTransaction);
}
async getCurrentWalletBalance(userId: string, restaurantId: string): Promise<number> {
const walletTransaction = await this.em.findOne(
WalletTransaction,
{
user: { id: userId },
restaurant: { id: restaurantId },
},
{ orderBy: { createdAt: 'desc' } },
);
return walletTransaction?.balance || 0;
}
}
+6 -10
View File
@@ -6,21 +6,17 @@ import { User } from './entities/user.entity';
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 { WalletTransactionRepository } from './repositories/wallet-transaction.repository';
import { CreditTransactionRepository } from './repositories/credit-transaction.repository';
import { WalletService } from './providers/wallet.service';
import { WalletTransaction } from './entities/wallet-transaction.entity';
import { PointTransaction } from './entities/point-transaction.entity';
import { PointTransactionRepository } from './repositories/point-transaction.repository';
import { CreditTransaction } from './entities/credit-transaction.entity';
@Module({
providers: [UserService, WalletService, UserRepository, WalletTransactionRepository, PointTransactionRepository],
providers: [UserService, WalletService, UserRepository, CreditTransactionRepository, CreditTransactionRepository],
controllers: [UsersController],
imports: [
MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction]),
MikroOrmModule.forFeature([User, UserAddress, CreditTransaction]),
JwtModule,
RestaurantsModule,
],
exports: [UserService, WalletService, UserRepository, WalletTransactionRepository, PointTransactionRepository],
exports: [UserService, WalletService, UserRepository, CreditTransactionRepository, CreditTransactionRepository],
})
export class UserModule {}
export class UserModule { }