From 7dfd42bd5b8dac25cfa554e700874e61e259e373 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 23 Jun 2026 11:11:25 +0330 Subject: [PATCH] add/update user --- .../users/controllers/users.controller.ts | 31 +++++++ src/modules/users/dto/create-user.dto.ts | 40 +++++++++ src/modules/users/providers/user.service.ts | 90 ++++++++++++++++++- 3 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 src/modules/users/dto/create-user.dto.ts diff --git a/src/modules/users/controllers/users.controller.ts b/src/modules/users/controllers/users.controller.ts index 6270a3a..c14b509 100644 --- a/src/modules/users/controllers/users.controller.ts +++ b/src/modules/users/controllers/users.controller.ts @@ -4,6 +4,7 @@ import { FileInterceptor, type File } from '@nest-lab/fastify-multer'; import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; import { UserService } from '../providers/user.service'; import { UpdateUserDto } from '../dto/update-user.dto'; +import { CreateUserDto } from '../dto/create-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'; @@ -184,6 +185,36 @@ export class UsersController { return this.userService.findPaginated(restId, query); } + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @ApiHeader(API_HEADER_SLUG) + @ApiOperation({ summary: 'Create a new user for the restaurant' }) + @ApiBody({ type: CreateUserDto }) + @Post('admin/users') + async createUser( + @RestId() restId: string, + @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: CreateUserDto, + ) { + return this.userService.createUserForRestaurant(restId, dto); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Permissions(Permission.MANAGE_USERS) + @ApiHeader(API_HEADER_SLUG) + @ApiOperation({ summary: 'Update a user (admin)' }) + @ApiParam({ name: 'userId', description: 'User ID' }) + @ApiBody({ type: UpdateUserDto }) + @Patch('admin/users/:userId') + async adminUpdateUser( + @Param('userId') userId: string, + @RestId() restId: string, + @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserDto, + ) { + return this.userService.adminUpdateUser(userId, restId, dto); + } + @UseGuards(AdminAuthGuard) @ApiBearerAuth() @Permissions(Permission.MANAGE_USERS) diff --git a/src/modules/users/dto/create-user.dto.ts b/src/modules/users/dto/create-user.dto.ts new file mode 100644 index 0000000..273f1f6 --- /dev/null +++ b/src/modules/users/dto/create-user.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsMobilePhone, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateUserDto { + @ApiProperty({ description: "User's phone number", example: '09121234567' }) + @IsNotEmpty() + @IsString() + @IsMobilePhone('fa-IR') + phone!: string; + + @ApiProperty({ description: "User's first name", example: 'John' }) + @IsNotEmpty() + @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() + @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; +} diff --git a/src/modules/users/providers/user.service.ts b/src/modules/users/providers/user.service.ts index 5f6ff64..ee5dc4b 100644 --- a/src/modules/users/providers/user.service.ts +++ b/src/modules/users/providers/user.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common'; import { FilterQuery, RequiredEntityData } from '@mikro-orm/core'; import type { File } from '@nest-lab/fastify-multer'; import { User } from '../entities/user.entity'; @@ -6,6 +6,7 @@ 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'; @@ -23,7 +24,7 @@ import { UserRestaurantRepository } from '../repositories/user-restuarant.reposi import { UserRestaurant } from '../entities/user-restuarant.entity'; import { ImportUsersResult } from '../interfaces/import-users.interface'; import { assertExcelMimeType, parseUsersExcel } from '../utils/import-users-excel.util'; -import { UserImportMessage } from 'src/common/enums/message.enum'; +import { AuthMessage, UserImportMessage } from 'src/common/enums/message.enum'; @Injectable() export class UserService { @@ -119,6 +120,91 @@ export class UserService { return user; } + async createUserForRestaurant(restId: string, dto: CreateUserDto): Promise { + const restaurant = await this.restaurantRepository.findOne({ id: restId }); + if (!restaurant) { + throw new NotFoundException(`Restaurant not found.`); + } + + const normalizedPhone = normalizePhone(dto.phone); + let user = await this.userRepository.findOne({ phone: normalizedPhone }); + + if (user) { + const existingLink = await this.userRestaurantRepository.findOne({ user, restaurant }); + if (existingLink) { + throw new ConflictException(AuthMessage.PHONE_EXISTS); + } + + const updates: Partial = { firstName: dto.firstName }; + if (dto.lastName !== undefined) { + updates.lastName = dto.lastName; + } + if (dto.gender !== undefined) { + updates.gender = dto.gender; + } + if (dto.avatarUrl !== undefined) { + updates.avatarUrl = dto.avatarUrl; + } + if (dto.birthDate) { + updates.birthDate = new Date(dto.birthDate); + } + if (dto.marriageDate) { + updates.marriageDate = new Date(dto.marriageDate); + } + this.em.assign(user, updates); + } else { + const createData = { + phone: normalizedPhone, + firstName: dto.firstName, + lastName: dto.lastName, + gender: dto.gender, + avatarUrl: dto.avatarUrl, + birthDate: dto.birthDate ? new Date(dto.birthDate) : undefined, + marriageDate: dto.marriageDate ? new Date(dto.marriageDate) : undefined, + } as unknown as RequiredEntityData; + + user = this.userRepository.create(createData); + + const points = Number(restaurant.score?.registerScore || 0); + if (points > 0) { + const newWalletTransaction = this.walletTransactionRepository.create({ + user, + restaurant, + balance: 0, + amount: points, + type: WalletTransactionType.CREDIT, + reason: WalletTransactionReason.DEPOSIT, + }); + this.em.persist(newWalletTransaction); + } + this.em.persist(user); + } + + const userRestaurant = this.userRestaurantRepository.create({ + user, + restaurant, + orderCount: 0, + totalOrderAmount: 0, + }); + this.em.persist(userRestaurant); + await this.em.flush(); + + await this.em.populate(userRestaurant, ['user']); + return userRestaurant; + } + + async adminUpdateUser(userId: string, restId: string, dto: UpdateUserDto): Promise { + const userRestaurant = await this.userRestaurantRepository.findOne({ + user: { id: userId }, + restaurant: { id: restId }, + }); + if (!userRestaurant) { + throw new NotFoundException(`User with ID ${userId} not found.`); + } + + return this.updateUser(userId, restId, dto); + } + async updateUser(userId: string, restId: string, dto: UpdateUserDto): Promise { const user = await this.userRepository.findOne({ id: userId }); if (!user) {