Compare commits

..

1 Commits

Author SHA1 Message Date
368e80408d add update booking settings API 2025-07-27 10:22:09 +03:00
7 changed files with 112 additions and 83 deletions

View File

@ -424,10 +424,6 @@ export class ControllerRoute {
public static readonly ROUTE = '/user';
static ACTIONS = class {
public static readonly GET_USERS_WITH_BOOKABLE_SPACES_SUMMARY =
'Retrieve list of users that has bookable spaces';
public static readonly GET_USERS_WITH_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint retrieves all the users that have access to bookable spaces, paginated & accepts sorting';
public static readonly GET_USER_DETAILS_SUMMARY =
'Retrieve user details by user UUID';
public static readonly GET_USER_DETAILS_DESCRIPTION =
@ -452,6 +448,11 @@ export class ControllerRoute {
public static readonly UPDATE_NAME_DESCRIPTION =
'This endpoint updates the name for a user identified by their UUID.';
public static readonly UPDATE_BOOKING_SETTINGS_SUMMARY =
'Update booking settings by user UUID';
public static readonly UPDATE_BOOKING_SETTINGS_DESCRIPTION =
'This endpoint updates the booking settings for a user identified by their UUID.';
public static readonly DELETE_USER_SUMMARY = 'Delete user by UUID';
public static readonly DELETE_USER_DESCRIPTION =
'This endpoint deletes a user identified by their UUID. Accessible only by users with the Super Admin role.';

View File

@ -11,6 +11,7 @@ import {
} from 'typeorm';
import { OtpType } from '../../../../src/constants/otp-type.enum';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { BookingEntity } from '../../booking/entities/booking.entity';
import { ClientEntity } from '../../client/entities';
import {
DeviceNotificationEntity,
@ -29,7 +30,6 @@ import {
UserOtpDto,
UserSpaceDto,
} from '../dtos';
import { BookingEntity } from '../../booking/entities/booking.entity';
@Entity({ name: 'user' })
export class UserEntity extends AbstractEntity<UserDto> {
@ -44,7 +44,6 @@ export class UserEntity extends AbstractEntity<UserDto> {
nullable: true,
type: 'text',
default: defaultProfilePicture,
select: false,
})
public profilePicture: string;
@ -102,6 +101,9 @@ export class UserEntity extends AbstractEntity<UserDto> {
@Column({ type: 'timestamp', nullable: true })
appAgreementAcceptedAt: Date;
@Column({ type: Boolean, default: false })
bookingEnabled: boolean;
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user, {
onDelete: 'CASCADE',
})

View File

@ -1,11 +1,11 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { UserParamDto } from '../dtos';
import { UserSpaceService } from '../services';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { UserParamDto } from '../dtos';
@ApiTags('User Module')
@Controller({

View File

@ -11,7 +11,6 @@ import {
Param,
Patch,
Put,
Query,
Req,
UseGuards,
} from '@nestjs/common';
@ -25,8 +24,8 @@ import {
UpdateRegionDataDto,
UpdateTimezoneDataDto,
} from '../dtos';
import { UsersWithBookableSpacesFilterDto } from '../dtos/users-with-bookable-spaces-filter.dto';
import { UserService } from '../services/user.service';
import { UpdateBookingSettingsDto } from '../dtos/update-user-booking-settings.dto';
@ApiTags('User Module')
@Controller({
@ -36,21 +35,6 @@ import { UserService } from '../services/user.service';
export class UserController {
constructor(private readonly userService: UserService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('with-bookable-spaces')
@ApiOperation({
summary:
ControllerRoute.USER.ACTIONS.GET_USERS_WITH_BOOKABLE_SPACES_SUMMARY,
description:
ControllerRoute.USER.ACTIONS.GET_USERS_WITH_BOOKABLE_SPACES_DESCRIPTION,
})
async getUsersWithBookableSpaces(
@Query() dto: UsersWithBookableSpacesFilterDto,
): Promise<BaseResponseDto> {
return this.userService.getUsersWithBookableSpaces(dto);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get(':userUuid')
@ -155,6 +139,30 @@ export class UserController {
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('/booking-settings/:userUuid')
@ApiOperation({
summary: ControllerRoute.USER.ACTIONS.UPDATE_BOOKING_SETTINGS_SUMMARY,
description:
ControllerRoute.USER.ACTIONS.UPDATE_BOOKING_SETTINGS_DESCRIPTION,
})
async updateBookingSettingsByUserUuid(
@Param('userUuid') userUuid: string,
@Body() dto: UpdateBookingSettingsDto,
) {
const userData = await this.userService.updateBookingSettingsByUserUuid(
userUuid,
dto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'Booking settings updated successfully',
data: userData,
};
}
@ApiBearerAuth()
@UseGuards(SuperAdminRoleGuard)
@Delete('/:userUuid')

View File

@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsInt, IsOptional, IsPositive, Min } from 'class-validator';
export class UpdateBookingSettingsDto {
@ApiProperty({
description: 'bookingEnable',
required: false,
})
@IsBoolean()
@IsOptional()
bookingEnable?: boolean;
@ApiProperty({
description: 'increase user booking balance by top up amount',
required: false,
})
@IsPositive()
@IsOptional()
topUp?: number;
@ApiProperty({
description: 'replace user booking balance by required amount',
required: false,
})
@IsInt()
@Min(0)
@IsOptional()
balance?: number;
}

View File

@ -1,21 +0,0 @@
import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto';
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsOptional, ValidateIf } from 'class-validator';
export class UsersWithBookableSpacesFilterDto extends PaginationRequestGetListDto {
@ApiProperty({
enum: ['accessStartDate', 'accessEndDate'],
required: false,
})
@IsEnum(['accessStartDate', 'accessEndDate'])
@ValidateIf((o) => o.sortDirection)
sortBy: 'accessStartDate' | 'accessEndDate';
@ApiProperty({
enum: ['asc', 'desc'],
required: false,
})
@IsEnum(['asc', 'desc'])
@IsOptional()
sortDirection: 'asc' | 'desc';
}

View File

@ -1,18 +1,16 @@
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { removeBase64Prefix } from '@app/common/helper/removeBase64Prefix';
import { RegionRepository } from '@app/common/modules/region/repositories';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import { UserEntity } from '@app/common/modules/user/entities';
import { UserRepository } from '@app/common/modules/user/repositories';
import { getPaginationResponseDto } from '@app/common/util/getPaginationResponseDto';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { UsersWithBookableSpacesFilterDto } from '../dtos/users-with-bookable-spaces-filter.dto';
import { UpdateBookingSettingsDto } from '../dtos/update-user-booking-settings.dto';
import {
UpdateNameDto,
UpdateProfilePictureDataDto,
@ -36,7 +34,7 @@ export class UserService {
relations: ['region', 'timezone', 'roleType', 'project'],
});
if (!user) {
throw new BadRequestException('Invalid room UUID');
throw new BadRequestException('Invalid user UUID');
}
// Use the utility function to remove the base64 prefix
@ -56,6 +54,8 @@ export class UserService {
appAgreementAcceptedAt: user?.appAgreementAcceptedAt,
role: user?.roleType,
project: user?.project,
bookingPoints: user?.bookingPoints ?? 0,
bookingEnabled: user?.bookingEnabled ?? false,
};
} catch (err) {
if (err instanceof BadRequestException) {
@ -66,38 +66,6 @@ export class UserService {
}
}
async getUsersWithBookableSpaces({
sortBy,
sortDirection,
page,
size,
}: UsersWithBookableSpacesFilterDto) {
size = size ?? 10;
page = page ?? 1;
const qb = this.userRepository
.createQueryBuilder('user')
.innerJoin('user.userSpaces', 'userSpaces')
.innerJoin('userSpaces.space', 'space')
.innerJoin('space.bookableConfig', 'bookableConfig')
.where('bookableConfig.uuid IS NOT NULL')
.leftJoinAndSelect('user.inviteUser', 'inviteUser')
.take(size)
.skip((page - 1) * size)
.distinct(true);
if (sortBy) {
qb.orderBy(
':sortBy',
sortDirection == 'desc' ? 'DESC' : 'ASC',
).setParameter('sortBy', sortBy);
}
const [data, count] = await qb.getManyAndCount();
return new PageResponse(
{ message: 'users fetched successfully', data },
getPaginationResponseDto(count, page, size),
);
}
async updateProfilePictureByUserUuid(
userUuid: string,
updateProfilePictureDataDto: UpdateProfilePictureDataDto,
@ -280,6 +248,48 @@ export class UserService {
);
}
}
async updateBookingSettingsByUserUuid(
userUuid: string,
{ balance, bookingEnable, topUp }: UpdateBookingSettingsDto,
) {
try {
let user = await this.getUserDetailsByUserUuid(userUuid);
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
if (balance && topUp) {
throw new BadRequestException(
'Please provide either balance or topUp, not both',
);
}
if (topUp) {
user.bookingPoints += topUp;
}
if (balance) {
user.bookingPoints = balance;
}
if (bookingEnable !== undefined) {
user.bookingEnabled = bookingEnable;
}
user = await this.userRepository.save(user);
return {
uuid: user.uuid,
bookingEnabled: user.bookingEnabled,
bookingPoints: user.bookingPoints,
};
} catch (err) {
throw new HttpException(
err.message || 'User not found',
HttpStatus.NOT_FOUND,
);
}
}
async acceptWebAgreement(userUuid: string) {
await this.userRepository.update(
{ uuid: userUuid },