From 5ef1352fb4cd0a7594cc920b7c9705675986e4d0 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Thu, 11 Apr 2024 11:39:18 +0600 Subject: [PATCH 01/26] [+] add dto --- .../office/effective/dto/BookingRequestDTO.kt | 14 + .../effective/dto/BookingResponseDTO.kt | 16 + .../booking/facade/BookingFacadeV1.kt | 123 ++++++ .../booking/routes/BookingRoutingV1.kt | 67 +++ .../routes/swagger/BookingSwaggerV1.kt | 384 ++++++++++++++++++ .../booking/service/BookingService.kt | 25 ++ 6 files changed, 629 insertions(+) create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingRequestDTO.kt create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingResponseDTO.kt create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingRequestDTO.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingRequestDTO.kt new file mode 100644 index 000000000..c202b9077 --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingRequestDTO.kt @@ -0,0 +1,14 @@ +package office.effective.dto + +import kotlinx.serialization.Serializable +import model.RecurrenceDTO + +@Serializable +data class BookingRequestDTO ( + val ownerEmail: String?, + val participantEmails: List, + val workspaceId: String, + val beginBooking: Long, + val endBooking: Long, + val recurrence: RecurrenceDTO? = null +) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingResponseDTO.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingResponseDTO.kt new file mode 100644 index 000000000..4e2692137 --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingResponseDTO.kt @@ -0,0 +1,16 @@ +package office.effective.dto + +import kotlinx.serialization.Serializable +import model.RecurrenceDTO + +@Serializable +data class BookingResponseDTO ( + val owner: UserDTO, + val participants: List, + val workspace: WorkspaceDTO, + val id: String, + val beginBooking: Long, + val endBooking: Long, + val recurrence: RecurrenceDTO? = null, + val recurringBookingId: String? = null +) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt new file mode 100644 index 000000000..b63a97e9f --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt @@ -0,0 +1,123 @@ +package office.effective.features.booking.facade + +import io.ktor.server.plugins.* +import office.effective.common.constants.BookingConstants +import office.effective.common.exception.InstanceNotFoundException +import office.effective.common.utils.DatabaseTransactionManager +import office.effective.common.utils.UuidValidator +import office.effective.features.booking.converters.BookingFacadeConverter +import office.effective.dto.BookingDTO +import office.effective.model.Booking +import office.effective.model.Workspace +import office.effective.serviceapi.IBookingService + +/** + * Class used in routes to handle bookings requests. + * Provides business transaction, data conversion and validation. + * + * In case of an error, the database transaction will be rolled back. + */ +class BookingFacadeV1( + private val bookingService: IBookingService, + private val transactionManager: DatabaseTransactionManager, + private val uuidValidator: UuidValidator, + private val bookingConverter: BookingFacadeConverter +) { + + /** + * Deletes the booking with the given id + * + * @param id booking id + * @author Daniil Zavyalov + */ + fun deleteById(id: String) { + transactionManager.useTransaction({ + bookingService.deleteById(id) + }) + } + + /** + * Retrieves a booking model by its id + * + * @param id id of requested booking + * @return [BookingDTO] with the given id + * @throws InstanceNotFoundException if booking with the given id doesn't exist in database + * @author Daniil Zavyalov + */ + fun findById(id: String): BookingDTO { + val dto: BookingDTO = transactionManager.useTransaction({ + val model = bookingService.findById(id) + ?: throw InstanceNotFoundException(Workspace::class, "Booking with id $id not found") + bookingConverter.modelToDto(model) + }) + return dto + } + + /** + * Returns all bookings. Bookings can be filtered by owner and workspace id + * + * @param userId use to filter by booking owner id. Should be valid UUID + * @param workspaceId use to filter by booking workspace id. Should be valid UUID + * @param bookingRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. + * Should be greater than range_from. + * @param bookingRangeFrom lower bound (exclusive) for a endBooking to filter by. + * Should be lover than [bookingRangeFrom]. Default value: [BookingConstants.MIN_SEARCH_START_TIME] + * @return [BookingDTO] list + * @author Daniil Zavyalov + */ + fun findAll( + userId: String?, + workspaceId: String?, + bookingRangeTo: Long?, + bookingRangeFrom: Long = BookingConstants.MIN_SEARCH_START_TIME + ): List { + if (bookingRangeTo != null && bookingRangeTo <= bookingRangeFrom) { + throw BadRequestException("Max booking start time should be null or greater than min start time") + } + val bookingList: List = transactionManager.useTransaction({ + bookingService.findAll( + userId?.let { uuidValidator.uuidFromString(it) }, + workspaceId?.let { uuidValidator.uuidFromString(it) }, + bookingRangeTo, + bookingRangeFrom + ) + }) + return bookingList.map { + bookingConverter.modelToDto(it) + } + } + + /** + * Saves a given booking. Use the returned model for further operations + * + * @param bookingDTO [BookingDTO] to be saved + * @return saved [BookingDTO] + * @author Daniil Zavyalov + */ + fun post(bookingDTO: BookingDTO): BookingDTO { + val model = bookingConverter.dtoToModel(bookingDTO) + val dto: BookingDTO = transactionManager.useTransaction({ + val savedModel = bookingService.save(model) + bookingConverter.modelToDto(savedModel) + }) + return dto + } + + /** + * Updates a given booking. Use the returned model for further operations + * + * @param bookingDTO changed booking + * @return [BookingDTO] after change saving + * @throws BadRequestException if booking id is null + * @author Daniil Zavyalov + */ + fun put(bookingDTO: BookingDTO): BookingDTO { + if (bookingDTO.id == null) throw BadRequestException("Missing booking id") + val model = bookingConverter.dtoToModel(bookingDTO) + val dto: BookingDTO = transactionManager.useTransaction({ + val savedModel = bookingService.update(model) + bookingConverter.modelToDto(savedModel) + }) + return dto + } +} \ No newline at end of file diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt new file mode 100644 index 000000000..1067b6799 --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt @@ -0,0 +1,67 @@ +package office.effective.features.booking.routes + +import io.github.smiley4.ktorswaggerui.dsl.delete +import io.github.smiley4.ktorswaggerui.dsl.get +import io.github.smiley4.ktorswaggerui.dsl.post +import io.github.smiley4.ktorswaggerui.dsl.put +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import office.effective.common.notifications.INotificationSender +import office.effective.common.swagger.SwaggerDocument +import office.effective.dto.BookingDTO +import office.effective.features.booking.facade.BookingFacade +import office.effective.features.booking.facade.BookingFacadeV1 +import office.effective.features.booking.routes.swagger.* +import org.koin.core.context.GlobalContext + +fun Route.bookingRoutingV1() { + route("/v1/bookings") { + val bookingFacade: BookingFacadeV1 = GlobalContext.get().get() + + get("/{id}", SwaggerDocument.returnBookingByIdV1()) { + val id: String = call.parameters["id"] + ?: return@get call.respond(HttpStatusCode.BadRequest) + call.respond(bookingFacade.findById(id)) + } + + get(SwaggerDocument.returnBookingsV1()) { + val userId: String? = call.request.queryParameters["user_id"] + val workspaceId: String? = call.request.queryParameters["workspace_id"] + val bookingRangeTo: Long? = call.request.queryParameters["range_to"]?.let { + it.toLongOrNull() + ?: throw BadRequestException("range_to can't be parsed to Long") + } + call.request.queryParameters["range_from"]?.let { + val bookingRangeFrom: Long = it.toLongOrNull() + ?: throw BadRequestException("range_from can't be parsed to Long") + call.respond(bookingFacade.findAll(userId, workspaceId, bookingRangeTo, bookingRangeFrom)) + return@get + } + call.respond(bookingFacade.findAll(userId, workspaceId, bookingRangeTo)) + } + post(SwaggerDocument.postBookingV1()) { + val dto = call.receive() + + call.response.status(HttpStatusCode.Created) + val result = bookingFacade.post(dto) + call.respond(result) + } + put(SwaggerDocument.putBookingV1()) { + val dto = call.receive() + + val result = bookingFacade.put(dto) + call.respond(result) + } + delete("{id}", SwaggerDocument.deleteBookingByIdV1()) { + val id: String = call.parameters["id"] + ?: return@delete call.respond(HttpStatusCode.BadRequest) + + bookingFacade.deleteById(id) + call.respond(HttpStatusCode.NoContent) + } + } +} \ No newline at end of file diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt new file mode 100644 index 000000000..fa278ff9a --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt @@ -0,0 +1,384 @@ +/** + * @suppress + */ +package office.effective.features.booking.routes.swagger + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.ktor.http.* +import office.effective.common.constants.BookingConstants +import office.effective.common.swagger.SwaggerDocument +import office.effective.dto.BookingDTO +import office.effective.dto.IntegrationDTO +import office.effective.dto.UserDTO +import office.effective.dto.UtilityDTO +import office.effective.dto.WorkspaceDTO +import java.time.Instant + +/** + * @suppress + */ +fun SwaggerDocument.returnBookingByIdV1(): OpenApiRoute.() -> Unit = { + description = "Returns booking found by id" + tags = listOf("bookings") + request { + pathParameter("id") { + description = "Booking id" + example = "c48c2a3d-bbfd-4801-b121-973ae3cf4cd9" + required = true + allowEmptyValue = false + } + } + response { + HttpStatusCode.OK to { + description = "Returns booking found by id" + body { + example( + "Bookings", bookingExample1 + ) {} + } + } + HttpStatusCode.BadRequest to { + description = "Bad request" + } + HttpStatusCode.NotFound to { + description = "Booking with this id was not found" + } + } +} + +/** + * @suppress + */ +fun SwaggerDocument.returnBookingsV1(): OpenApiRoute.() -> Unit = { + description = "Return all bookings. Bookings can be filtered by booking owner id, workspace id and time range. " + + "Returns only non-recurring bookings (recurring bookings are expanded into non-recurring ones). " + + "Can return no more than 2500 bookings." + tags = listOf("bookings") + request { + queryParameter("user_id") { + description = "Booking owner id" + example = "2c77feee-2bc1-11ee-be56-0242ac120002" + required = false + allowEmptyValue = false + } + queryParameter("workspace_id") { + description = "Booked workspace id" + example = "50d89406-2bc6-11ee-be56-0242ac120002" + required = false + allowEmptyValue = false + } + queryParameter("range_from") { + description = "Lower bound (exclusive) for a endBooking to filter by. Should be lover than range_to. " + + "Default value: ${BookingConstants.MIN_SEARCH_START_TIME} " + + "(${Instant.ofEpochMilli(BookingConstants.MIN_SEARCH_START_TIME)}). " + + "Old Google calendar events may not appear correctly in the system and cause unexpected exceptions" + example = 1692927200000 + required = false + allowEmptyValue = false + + } + queryParameter("range_to") { + description = "Upper bound (exclusive) for a beginBooking to filter by. Optional. " + + "Should be greater than range_from." + example = 1697027200000 + required = false + allowEmptyValue = false + } + } + response { + HttpStatusCode.OK to { + description = "Returns all bookings found by user id" + body> { + example( + "Workspace", listOf( + bookingExample1, bookingExample2 + ) + ) {} + } + } + HttpStatusCode.BadRequest to { + description = "range_to isn't greater then range_to, or one of them can't be parsed to Long" + } + HttpStatusCode.NotFound to { + description = "User or workspace with the given id doesn't exist" + } + } +} + +/** + * @suppress + */ +fun SwaggerDocument.postBookingV1(): OpenApiRoute.() -> Unit = { + description = "Saves a given booking" + tags = listOf("bookings") + request { + body { + example( + "Bookings", BookingDTO( + owner = UserDTO( + id = "2c77feee-2bc1-11ee-be56-0242ac120002", + fullName = "Max", + active = true, + role = "Fullstack developer", + avatarUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + integrations = listOf( + IntegrationDTO( + "c717cf6e-28b3-4148-a469-032991e5d9e9", + "phoneNumber", + "89087659880" + ) + ), + email = "cool.fullstack.developer@effective.band", + tag = "employee" + ), + participants = listOf( + UserDTO( + id = "2c77feee-2bc1-11ee-be56-0242ac120002", + fullName = "Ivan Ivanov", + active = true, + role = "Android developer", + avatarUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + integrations = listOf( + IntegrationDTO( + "c717cf6e-28b3-4148-a469-032991e5d9e9", + "phoneNumber", + "89236379887" + ) + ), + email = "cool.backend.developer@effective.band", + tag = "employee" + ) + ), + workspace = WorkspaceDTO( + id = "2561471e-2bc6-11ee-be56-0242ac120002", name = "Sun", tag = "meeting", + utilities = listOf( + UtilityDTO( + id = "50d89406-2bc6-11ee-be56-0242ac120002", + name = "Sockets", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 8 + ), UtilityDTO( + id = "a62a86c6-2bc6-11ee-be56-0242ac120002", + name = "Projectors", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 1 + ) + ) + ), + id = null, + beginBooking = 1691299526000, + endBooking = 1691310326000, + recurrence = null + ) + ) + } + } + response { + HttpStatusCode.Created to { + description = "Returns saved booking" + body { + example( + "Bookings", bookingExample2 + ) {} + } + } + HttpStatusCode.BadRequest to { + description = "Invalid request body or workspace can't be booked in a given period" + } + HttpStatusCode.NotFound to { + description = "User or workspace with the given id doesn't exist" + } + } +} + +/** + * @suppress + */ +fun SwaggerDocument.putBookingV1(): OpenApiRoute.() -> Unit = { + description = "Updates a given booking" + tags = listOf("bookings") + request { + body { + example( + "Bookings", bookingExample1 + ) + } + } + response { + HttpStatusCode.OK to { + description = "Returns saved booking" + body { + example( + "Bookings", bookingExample1 + ) {} + } + } + HttpStatusCode.BadRequest to { + description = "Invalid request body or workspace can't be booked in a given period" + } + HttpStatusCode.NotFound to { + description = "Booking, user or workspace with the given id doesn't exist" + } + } +} + +/** + * @suppress + */ +fun SwaggerDocument.deleteBookingByIdV1(): OpenApiRoute.() -> Unit = { + description = + "Deletes a booking with the given id. If the booking is not found in the database it is silently ignored" + tags = listOf("bookings") + request { + pathParameter("id") { + description = "Booking id" + example = "c48c2a3d-bbfd-4801-b121-973ae3cf4cd9" + required = true + allowEmptyValue = false + } + } + response { + HttpStatusCode.NoContent to { + description = "Booking was successfully deleted" + } + HttpStatusCode.BadRequest to { + description = "Bad request" + } + } +} + +/** + * @suppress + */ +private val bookingExample1 = BookingDTO( + owner = UserDTO( + id = "2c77feee-2bc1-11ee-be56-0242ac120002", + fullName = "Ivan Ivanov", + active = true, + role = "Backend developer", + avatarUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + integrations = listOf( + IntegrationDTO( + "c717cf6e-28b3-4148-a469-032991e5d9e9", + "phoneNumber", + "89236379887" + ) + ), + email = "cool.backend.developer@effective.band", + tag = "employee" + ), + participants = listOf( + UserDTO( + id = "2c77feee-2bc1-11ee-be56-0242ac120002", + fullName = "Ivan Ivanov", + active = true, + role = "Backend developer", + avatarUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + integrations = listOf( + IntegrationDTO( + "c717cf6e-28b3-4148-a469-032991e5d9e9", + "phoneNumber", + "89236379887" + ) + ), + email = "cool.backend.developer@effective.band", + tag = "employee" + ), + UserDTO( + id = "207b9634-2bc4-11ee-be56-0242ac120002", + fullName = "Grzegorz Brzęczyszczykiewicz", + active = true, + role = "Guest", + avatarUrl = "https://img.freepik.com/free-photo/capybara-in-the-nature-habitat-of-northern-pantanal_475641-1029.jpg", + integrations = listOf( + IntegrationDTO( + "c717cf6e-28b3-4148-a469-032991e5d9e9", + "phoneNumber", + "89086379880" + ) + ), + email = "email@yahoo.com", + tag = "employee" + ) + ), + workspace = WorkspaceDTO( + id = "2561471e-2bc6-11ee-be56-0242ac120002", name = "Sun", tag = "meeting", + utilities = listOf( + UtilityDTO( + id = "50d89406-2bc6-11ee-be56-0242ac120002", + name = "Sockets", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 8 + ), UtilityDTO( + id = "a62a86c6-2bc6-11ee-be56-0242ac120002", + name = "Projectors", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 1 + ) + ) + ), + id = "c48c2a3d-bbfd-4801-b121-973ae3cf4cd9", + beginBooking = 1691299526000, + endBooking = 1691310326000, + recurrence = null +) + +/** + * @suppress + */ +private val bookingExample2 = BookingDTO( + owner = UserDTO( + id = "2c77feee-2bc1-11ee-be56-0242ac120002", + fullName = "Ivan Ivanov", + active = true, + role = "Backend developer", + avatarUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + integrations = listOf( + IntegrationDTO( + "c717cf6e-28b3-4148-a469-032991e5d9e9", + "phoneNumber", + "89236379887" + ) + ), + email = "cool.backend.developer@effective.band", + tag = "employee" + ), + participants = listOf( + UserDTO( + id = "2c77feee-2bc1-11ee-be56-0242ac120002", + fullName = "Ivan Ivanov", + active = true, + role = "Backend developer", + avatarUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + integrations = listOf( + IntegrationDTO( + "c717cf6e-28b3-4148-a469-032991e5d9e9", + "phoneNumber", + "89236379887" + ) + ), + email = "cool.backend.developer@effective.band", + tag = "employee" + ) + ), + workspace = WorkspaceDTO( + id = "2561471e-2bc6-11ee-be56-0242ac120002", name = "Sun", tag = "meeting", + utilities = listOf( + UtilityDTO( + id = "50d89406-2bc6-11ee-be56-0242ac120002", + name = "Sockets", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 8 + ), UtilityDTO( + id = "a62a86c6-2bc6-11ee-be56-0242ac120002", + name = "Projectors", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 1 + ) + ) + ), + id = "c48c2a3d-bbfd-4801-b121-973ae3cf4cd9", + beginBooking = 1691299526000, + endBooking = 1691310326000, + recurrence = null +) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt index d67893ecf..176f78b68 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt @@ -49,6 +49,31 @@ class BookingService( bookingWorkspaceRepository.deleteById(id) } + /** + * Retrieves a booking model by its id + * + * @param id - booking id + * @return [Booking] with the given [id] or null if workspace with the given id doesn't exist + * @author Daniil Zavyalov + */ + override fun findByIdV0(id: String): Booking? { + val booking = bookingRepository.findById(id) + ?: bookingWorkspaceRepository.findById(id) + ?: return null + val userIds = mutableSetOf() + for (participant in booking.participants) { + participant.id?.let { userIds.add(it) } + } + booking.owner.id?.let { userIds.add(it) } + val integrations = userRepository.findAllIntegrationsByUserIds(userIds) + booking.workspace.utilities = findUtilities(booking.workspace) + booking.owner.integrations = integrations[booking.owner.id] ?: setOf() + for (participant in booking.participants) { + participant.integrations = integrations[participant.id] ?: setOf() + } + return booking + } + /** * Retrieves a booking model by its id * From d49a057aac017b98a66d5e44c783dbc609f7f488 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Thu, 11 Apr 2024 23:30:01 +0600 Subject: [PATCH 02/26] [+] add get request for api V1 --- .../converters/BookingFacadeConverter.kt | 84 ++++++++++++++++++- .../features/booking/di/BookingDiModule.kt | 10 +-- .../features/booking/facade/BookingFacade.kt | 7 ++ .../booking/facade/BookingFacadeV1.kt | 15 ++-- ...ository.kt => BookingMeetingRepository.kt} | 6 +- ...ository.kt => BookingRegularRepository.kt} | 5 +- .../booking/routes/BookingRoutingV1.kt | 2 - .../booking/service/BookingService.kt | 71 +++++----------- .../repository/WorkspaceRepository.kt | 8 +- .../kotlin/office/effective/model/Booking.kt | 5 +- 10 files changed, 136 insertions(+), 77 deletions(-) rename effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/{BookingCalendarRepository.kt => BookingMeetingRepository.kt} (99%) rename effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/{BookingWorkspaceRepository.kt => BookingRegularRepository.kt} (99%) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingFacadeConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingFacadeConverter.kt index 3c4c2fde5..4a0eb014d 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingFacadeConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingFacadeConverter.kt @@ -1,9 +1,14 @@ package office.effective.features.booking.converters import model.RecurrenceDTO +import office.effective.common.exception.InstanceNotFoundException import office.effective.dto.BookingDTO +import office.effective.dto.BookingRequestDTO +import office.effective.dto.BookingResponseDTO import office.effective.features.user.converters.UserDTOModelConverter +import office.effective.features.user.repository.UserRepository import office.effective.features.workspace.converters.WorkspaceFacadeConverter +import office.effective.features.workspace.repository.WorkspaceRepository import office.effective.model.* import org.slf4j.LoggerFactory import java.time.Instant @@ -13,8 +18,12 @@ import java.time.Instant * * Uses [UserDTOModelConverter] and [WorkspaceFacadeConverter] to convert contained users and workspaces */ -class BookingFacadeConverter(private val userConverter: UserDTOModelConverter, - private val workspaceConverter: WorkspaceFacadeConverter) { +class BookingFacadeConverter( + private val userConverter: UserDTOModelConverter, + private val workspaceConverter: WorkspaceFacadeConverter, + private val userRepository: UserRepository, + private val workspaceRepository: WorkspaceRepository +) { private val logger = LoggerFactory.getLogger(this::class.java) /** @@ -24,6 +33,10 @@ class BookingFacadeConverter(private val userConverter: UserDTOModelConverter, * @return The resulting [BookingDTO] object * @author Daniil Zavyalov, Danil Kiselev */ + @Deprecated( + message = "Deprecated since 1.0 api version", + replaceWith = ReplaceWith("modelToResponseDto(booking)") + ) fun modelToDto(booking: Booking): BookingDTO { logger.trace("Converting booking model to dto") var recurrenceDTO : RecurrenceDTO? = null @@ -41,6 +54,31 @@ class BookingFacadeConverter(private val userConverter: UserDTOModelConverter, ) } + /** + * Converts [Booking] to [BookingResponseDTO] + * + * @param booking [Booking] to be converted + * @return The resulting [BookingResponseDTO] object + * @author Daniil Zavyalov, Danil Kiselev + */ + fun modelToResponseDto(booking: Booking): BookingResponseDTO { + logger.trace("Converting booking model to response dto") + var recurrenceDTO : RecurrenceDTO? = null + if(booking.recurrence != null) { + recurrenceDTO = RecurrenceConverter.modelToDto(booking.recurrence!!) + } + return BookingResponseDTO( + owner = userConverter.modelToDTO(booking.owner), + participants = booking.participants.map { userConverter.modelToDTO(it) }, + workspace = workspaceConverter.modelToDto(booking.workspace), + id = booking.id.toString(), + beginBooking = booking.beginBooking.toEpochMilli(), + endBooking = booking.endBooking.toEpochMilli(), + recurrence = recurrenceDTO, + recurringBookingId = booking.recurringBookingId + ) + } + /** * Converts [BookingDTO] to [Booking] * @@ -48,6 +86,10 @@ class BookingFacadeConverter(private val userConverter: UserDTOModelConverter, * @return The resulting [Booking] object * @author Daniil Zavyalov, Danil Kiselev */ + @Deprecated( + message = "Deprecated since 1.0 api version", + replaceWith = ReplaceWith("requestDtoToModel(booking)") + ) fun dtoToModel(bookingDTO: BookingDTO): Booking { logger.trace("Converting booking dto to model") var recurrenceModel : RecurrenceModel? = null @@ -64,4 +106,42 @@ class BookingFacadeConverter(private val userConverter: UserDTOModelConverter, recurrence = recurrenceModel ) } + + /** + * Converts [BookingDTO] to [Booking] + * + * @param bookingDTO [BookingDTO] to be converted + * @return The resulting [Booking] object + * @author Daniil Zavyalov, Danil Kiselev + */ + fun requestDtoToModel(bookingDto: BookingRequestDTO, id: String? = null): Booking { + logger.trace("Converting booking response dto to model") + var recurrenceModel : RecurrenceModel? = null + if(bookingDto.recurrence != null) { + recurrenceModel = RecurrenceConverter.dtoToModel(bookingDto.recurrence) + } + val owner: UserModel? = findOwner(bookingDto.ownerEmail) + val participants = findParticipants() + return Booking( + owner = owner, + participants = bookingDto.participants.map { userConverter.dTOToModel(it) }, + workspace = workspaceConverter.dtoToModel(bookingDto.workspace), + id = id, + beginBooking = Instant.ofEpochMilli(bookingDto.beginBooking), + endBooking = Instant.ofEpochMilli(bookingDto.endBooking), + recurrence = recurrenceModel, + ) + } + + private fun findOwner(ownerEmail: String?): UserModel? { + if (ownerEmail != null) { + return userRepository.findByEmail(ownerEmail) + ?: throw InstanceNotFoundException(UserModel::class, "User with email $ownerEmail not found") + } + return null; + } + + private fun findParticipants(participantEmails: List): List { + + } } \ No newline at end of file diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt index 5cca49b3d..d3529b69b 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt @@ -3,19 +3,19 @@ package office.effective.features.booking.di import office.effective.features.booking.converters.BookingFacadeConverter import office.effective.features.booking.converters.BookingRepositoryConverter import office.effective.features.booking.facade.BookingFacade -import office.effective.features.booking.repository.BookingCalendarRepository +import office.effective.features.booking.repository.BookingMeetingRepository import office.effective.features.booking.service.BookingService import office.effective.features.booking.converters.GoogleCalendarConverter -import office.effective.features.booking.repository.BookingWorkspaceRepository +import office.effective.features.booking.repository.BookingRegularRepository import office.effective.serviceapi.IBookingService import org.koin.dsl.module val bookingDiModule = module(createdAtStart = true) { single { BookingRepositoryConverter(get(), get(), get(), get()) } single { GoogleCalendarConverter(get(), get(), get(), get(), get(), get(), get()) } - single { BookingWorkspaceRepository(get(), get(), get(), get()) } - single { BookingCalendarRepository(get(), get(), get(), get()) } + single { BookingRegularRepository(get(), get(), get(), get()) } + single { BookingMeetingRepository(get(), get(), get(), get()) } single { BookingService(get(), get(), get(), get()) } - single { BookingFacadeConverter(get(), get()) } + single { BookingFacadeConverter(get(), get(), get(), get()) } single { BookingFacade(get(), get(), get(), get()) } } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt index 3ccd277e4..ea10c5a0d 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt @@ -17,6 +17,13 @@ import office.effective.serviceapi.IBookingService * * In case of an error, the database transaction will be rolled back. */ +@Deprecated( + message = "Deprecated since 1.0 api version", + replaceWith = ReplaceWith( + expression = "BookingFacadeV1", + imports = ["office.effective.features.booking.facade.BookingFacadeV1"] + ) +) class BookingFacade( private val bookingService: IBookingService, private val transactionManager: DatabaseTransactionManager, diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt index b63a97e9f..6d73e0568 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt @@ -7,6 +7,7 @@ import office.effective.common.utils.DatabaseTransactionManager import office.effective.common.utils.UuidValidator import office.effective.features.booking.converters.BookingFacadeConverter import office.effective.dto.BookingDTO +import office.effective.dto.BookingResponseDTO import office.effective.model.Booking import office.effective.model.Workspace import office.effective.serviceapi.IBookingService @@ -40,15 +41,15 @@ class BookingFacadeV1( * Retrieves a booking model by its id * * @param id id of requested booking - * @return [BookingDTO] with the given id + * @return [BookingResponseDTO] with the given id * @throws InstanceNotFoundException if booking with the given id doesn't exist in database * @author Daniil Zavyalov */ - fun findById(id: String): BookingDTO { - val dto: BookingDTO = transactionManager.useTransaction({ + fun findById(id: String): BookingResponseDTO { + val dto: BookingResponseDTO = transactionManager.useTransaction({ val model = bookingService.findById(id) ?: throw InstanceNotFoundException(Workspace::class, "Booking with id $id not found") - bookingConverter.modelToDto(model) + bookingConverter.modelToResponseDto(model) }) return dto } @@ -70,7 +71,7 @@ class BookingFacadeV1( workspaceId: String?, bookingRangeTo: Long?, bookingRangeFrom: Long = BookingConstants.MIN_SEARCH_START_TIME - ): List { + ): List { if (bookingRangeTo != null && bookingRangeTo <= bookingRangeFrom) { throw BadRequestException("Max booking start time should be null or greater than min start time") } @@ -82,8 +83,8 @@ class BookingFacadeV1( bookingRangeFrom ) }) - return bookingList.map { - bookingConverter.modelToDto(it) + return bookingList.map { booking -> + bookingConverter.modelToResponseDto(booking) } } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingCalendarRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt similarity index 99% rename from effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingCalendarRepository.kt rename to effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt index f3cc643b6..a8e431f19 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingCalendarRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt @@ -4,7 +4,6 @@ import com.google.api.client.googleapis.json.GoogleJsonResponseException import com.google.api.client.util.DateTime import com.google.api.services.calendar.Calendar import com.google.api.services.calendar.model.Event -import kotlinx.coroutines.* import office.effective.common.constants.BookingConstants import office.effective.common.exception.InstanceNotFoundException import office.effective.common.exception.MissingIdException @@ -25,7 +24,7 @@ import java.util.concurrent.Executors * * Filters out all events that have a start less than the calendar.minTime from application.conf */ -class BookingCalendarRepository( +class BookingMeetingRepository( private val calendarIdsRepository: CalendarIdsRepository, private val userRepository: UserRepository, private val calendar: Calendar, @@ -57,8 +56,7 @@ class BookingCalendarRepository( */ override fun existsById(id: String): Boolean { logger.debug("[existsById] checking whether a booking with id={} exists", id) - val event: Any? - event = findByCalendarIdAndBookingId(id) + val event: Any? = findByCalendarIdAndBookingId(id) return event != null } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingWorkspaceRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt similarity index 99% rename from effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingWorkspaceRepository.kt rename to effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt index 9485a3347..4619d6708 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingWorkspaceRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt @@ -22,7 +22,7 @@ import java.util.* * * Filters out all events that have a start less than the calendar.minTime from application.conf */ -class BookingWorkspaceRepository( +class BookingRegularRepository( private val calendar: Calendar, private val googleCalendarConverter: GoogleCalendarConverter, private val workspaceRepository: WorkspaceRepository, @@ -41,8 +41,7 @@ class BookingWorkspaceRepository( */ override fun existsById(id: String): Boolean { logger.debug("[existsById] checking whether a booking with id={} exists", id) - val event: Any? - event = findByCalendarIdAndBookingId(id) + val event: Any? = findByCalendarIdAndBookingId(id) return event != null } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt index 1067b6799..4e06c5b34 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt @@ -10,10 +10,8 @@ import io.ktor.server.plugins.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -import office.effective.common.notifications.INotificationSender import office.effective.common.swagger.SwaggerDocument import office.effective.dto.BookingDTO -import office.effective.features.booking.facade.BookingFacade import office.effective.features.booking.facade.BookingFacadeV1 import office.effective.features.booking.routes.swagger.* import org.koin.core.context.GlobalContext diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt index 176f78b68..7c1a2ad07 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt @@ -2,8 +2,8 @@ package office.effective.features.booking.service import office.effective.common.exception.InstanceNotFoundException import office.effective.common.exception.MissingIdException -import office.effective.features.booking.repository.BookingCalendarRepository -import office.effective.features.booking.repository.BookingWorkspaceRepository +import office.effective.features.booking.repository.BookingMeetingRepository +import office.effective.features.booking.repository.BookingRegularRepository import office.effective.features.booking.repository.WorkspaceBookingEntity import office.effective.features.user.repository.UserEntity import office.effective.features.user.repository.UserRepository @@ -16,12 +16,12 @@ import java.util.UUID /** * Class that implements the [IBookingService] methods * - * Uses [BookingCalendarRepository] for operations with meeting rooms. - * Uses [BookingWorkspaceRepository] for operations with workspaces. + * Uses [BookingMeetingRepository] for operations with meeting rooms. + * Uses [BookingRegularRepository] for operations with workspaces. */ class BookingService( - private val bookingRepository: BookingCalendarRepository, - private val bookingWorkspaceRepository: BookingWorkspaceRepository, + private val bookingMeetingRepository: BookingMeetingRepository, + private val bookingRegularRepository: BookingRegularRepository, private val userRepository: UserRepository, private val workspaceRepository: WorkspaceRepository, ) : IBookingService { @@ -35,7 +35,7 @@ class BookingService( * @author Daniil Zavyalov */ override fun existsById(id: String): Boolean { - return bookingRepository.existsById(id) || bookingWorkspaceRepository.existsById(id) + return bookingMeetingRepository.existsById(id) || bookingRegularRepository.existsById(id) } /** @@ -45,33 +45,8 @@ class BookingService( * @author Daniil Zavyalov */ override fun deleteById(id: String) { - bookingRepository.deleteById(id) - bookingWorkspaceRepository.deleteById(id) - } - - /** - * Retrieves a booking model by its id - * - * @param id - booking id - * @return [Booking] with the given [id] or null if workspace with the given id doesn't exist - * @author Daniil Zavyalov - */ - override fun findByIdV0(id: String): Booking? { - val booking = bookingRepository.findById(id) - ?: bookingWorkspaceRepository.findById(id) - ?: return null - val userIds = mutableSetOf() - for (participant in booking.participants) { - participant.id?.let { userIds.add(it) } - } - booking.owner.id?.let { userIds.add(it) } - val integrations = userRepository.findAllIntegrationsByUserIds(userIds) - booking.workspace.utilities = findUtilities(booking.workspace) - booking.owner.integrations = integrations[booking.owner.id] ?: setOf() - for (participant in booking.participants) { - participant.integrations = integrations[participant.id] ?: setOf() - } - return booking + bookingMeetingRepository.deleteById(id) + bookingRegularRepository.deleteById(id) } /** @@ -82,8 +57,8 @@ class BookingService( * @author Daniil Zavyalov */ override fun findById(id: String): Booking? { - val booking = bookingRepository.findById(id) - ?: bookingWorkspaceRepository.findById(id) + val booking = bookingMeetingRepository.findById(id) + ?: bookingRegularRepository.findById(id) ?: return null val userIds = mutableSetOf() for (participant in booking.participants) { @@ -126,14 +101,14 @@ class BookingService( ) if (workspace.tag == "meeting") { - bookingRepository.findAllByOwnerAndWorkspaceId( + bookingMeetingRepository.findAllByOwnerAndWorkspaceId( userId, workspaceId, bookingRangeFrom, bookingRangeTo ) } else { - bookingWorkspaceRepository.findAllByOwnerAndWorkspaceId( + bookingRegularRepository.findAllByOwnerAndWorkspaceId( userId, workspaceId, bookingRangeFrom, @@ -147,13 +122,13 @@ class BookingService( UserEntity::class, "User with id $userId not found", userId ) - val bookings = bookingRepository.findAllByOwnerId( + val bookings = bookingMeetingRepository.findAllByOwnerId( userId, bookingRangeFrom, bookingRangeTo ).toMutableList() bookings.addAll( - bookingWorkspaceRepository.findAllByOwnerId( + bookingRegularRepository.findAllByOwnerId( userId, bookingRangeFrom, bookingRangeTo @@ -169,13 +144,13 @@ class BookingService( ) if (workspace.tag == "meeting") { - bookingRepository.findAllByWorkspaceId( + bookingMeetingRepository.findAllByWorkspaceId( workspaceId, bookingRangeFrom, bookingRangeTo ) } else { - bookingWorkspaceRepository.findAllByWorkspaceId( + bookingRegularRepository.findAllByWorkspaceId( workspaceId, bookingRangeFrom, bookingRangeTo @@ -185,8 +160,8 @@ class BookingService( } else -> { - val bookings = bookingRepository.findAll(bookingRangeFrom, bookingRangeTo).toMutableList() - bookings.addAll(bookingWorkspaceRepository.findAll(bookingRangeFrom, bookingRangeTo)) + val bookings = bookingMeetingRepository.findAll(bookingRangeFrom, bookingRangeTo).toMutableList() + bookings.addAll(bookingRegularRepository.findAll(bookingRangeFrom, bookingRangeTo)) bookings } } @@ -289,10 +264,10 @@ class BookingService( ?: throw InstanceNotFoundException(WorkspaceBookingEntity::class, "Workspace with id $workspaceId not wound") return if (workspace.tag == "meeting") { logger.error("Saving meeting room booking") - bookingRepository.save(booking) + bookingMeetingRepository.save(booking) } else { logger.error("Saving workspace booking") - bookingWorkspaceRepository.save(booking) + bookingRegularRepository.save(booking) } } @@ -310,10 +285,10 @@ class BookingService( ?: throw InstanceNotFoundException(WorkspaceBookingEntity::class, "Workspace with id $workspaceId not wound") return if (workspace.tag == "meeting") { logger.error("Updating meeting room booking") - bookingRepository.update(booking) + bookingMeetingRepository.update(booking) } else { logger.error("Updating workspace booking") - bookingWorkspaceRepository.update(booking) + bookingRegularRepository.update(booking) } } } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/repository/WorkspaceRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/repository/WorkspaceRepository.kt index 60f13e2dd..27f14f4fc 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/repository/WorkspaceRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/repository/WorkspaceRepository.kt @@ -1,8 +1,8 @@ package office.effective.features.workspace.repository import office.effective.common.exception.InstanceNotFoundException -import office.effective.features.booking.repository.BookingCalendarRepository -import office.effective.features.booking.repository.BookingWorkspaceRepository +import office.effective.features.booking.repository.BookingMeetingRepository +import office.effective.features.booking.repository.BookingRegularRepository import office.effective.features.booking.repository.IBookingRepository import office.effective.features.workspace.converters.WorkspaceRepositoryConverter import office.effective.model.Booking @@ -191,9 +191,9 @@ class WorkspaceRepository(private val database: Database, private val converter: ) val googleRepository: IBookingRepository = if (tag == "meeting") { - GlobalContext.get().get() //TODO: Fix global context call + GlobalContext.get().get() //TODO: Fix global context call } else { - GlobalContext.get().get() //TODO: Fix global context call + GlobalContext.get().get() //TODO: Fix global context call } val freeWorkspaces = mutableListOf() val bookings = googleRepository.findAll(beginTimestamp.toEpochMilli(), endTimestamp.toEpochMilli()) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/model/Booking.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/model/Booking.kt index c6b893d06..fc01cc8f2 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/model/Booking.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/model/Booking.kt @@ -3,11 +3,12 @@ package office.effective.model import java.time.Instant data class Booking ( - var owner: UserModel, + var owner: UserModel?, var participants: List, var workspace: Workspace, var id: String?, var beginBooking: Instant, var endBooking: Instant, - var recurrence: RecurrenceModel? + var recurrence: RecurrenceModel?, + val recurringBookingId: String? = null ) \ No newline at end of file From 1d185d1316896fa44a3d3ec7730fe62cc423f2aa Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Fri, 12 Apr 2024 19:36:22 +0600 Subject: [PATCH 03/26] [~] fix same event collision and refactor code --- .../repository/BookingMeetingRepository.kt | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt index a8e431f19..17858e489 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt @@ -410,42 +410,56 @@ class BookingMeetingRepository( "[checkBookingAvailable] checking if workspace with calendar id={} available for event {}", workspaceCalendar, incomingEvent - ) - var isAvailable = false; + )//TODO: нам не обязательно сохранять ивент перед коллизией, если он не циклический + var result = true if (incomingEvent.recurrence != null) { //TODO: Check, if we can receive instances without pushing this event into calendar - calendarEvents.instances(workspaceCalendar, incomingEvent.id).setMaxResults(50).execute().items.forEach { event -> - if (!checkSingleEventCollision(event, workspaceCalendar)) { - return@checkBookingAvailable false - } else { - isAvailable = true + val instances = calendarEvents.instances(workspaceCalendar, incomingEvent.id) + .setMaxResults(50) + .execute().items + + for (instance in instances) { + if (singleEventHasCollision(instance, workspaceCalendar)) { + result = false } } } else { - isAvailable = checkSingleEventCollision(incomingEvent, workspaceCalendar) + result = !singleEventHasCollision(incomingEvent, workspaceCalendar) } - logger.debug("[checkBookingAvailable] result {}", true) - return isAvailable + logger.debug("[checkBookingAvailable] result {}", result) + return result } /** - * Contains collision condition. Checks collision between single event from param and all saved events from event.start (leftBorder) until event.end (rightBorder) + * Checks weather the saved event has collision with other events. * - * @param event: [Event] - Must take only SAVED event + * @param eventToVerify: [Event] - event for collision check. Must be saved before check * */ - private fun checkSingleEventCollision(event: Event, workspaceCalendar: String): Boolean { - val savedEvents = basicQuery(event.start.dateTime.value, event.end.dateTime.value, true, workspaceCalendar) - .execute().items - for (i in savedEvents) { - if ( - !((i.start.dateTime.value >= event.end.dateTime.value) || (i.end.dateTime.value <= event.start.dateTime.value)) - && (i.id != event.id) - ) { - return false + private fun singleEventHasCollision(eventToVerify: Event, workspaceCalendar: String): Boolean { + val sameTimeEvents = basicQuery( + timeMin = eventToVerify.start.dateTime.value, + timeMax = eventToVerify.end.dateTime.value, + singleEvents = true, + calendarId = workspaceCalendar + ).execute().items + + for (event in sameTimeEvents) { + if (areEventsOverlap(eventToVerify, event) && eventsIsNotSame(eventToVerify, event)) { + return true } } + return false + } + + private fun areEventsOverlap(firstEvent: Event, secondEvent: Event): Boolean { + return secondEvent.start.dateTime.value < firstEvent.end.dateTime.value + && secondEvent.end.dateTime.value > firstEvent.start.dateTime.value + } - return true + private fun eventsIsNotSame(firstEvent: Event, secondEvent: Event): Boolean { + return firstEvent.id != secondEvent.id && + firstEvent.id != secondEvent.recurringEventId && + firstEvent.recurringEventId != secondEvent.id } } \ No newline at end of file From eda38c8ed7cdff6417f5392be6c584abfcd91fca Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sat, 13 Apr 2024 20:12:51 +0600 Subject: [PATCH 04/26] [~] refactor booking model-dto converter --- .../effective/dto/BookingResponseDTO.kt | 2 +- ...nverter.kt => BookingDtoModelConverter.kt} | 65 +++++++++++++------ .../converters/GoogleCalendarConverter.kt | 2 +- .../features/booking/di/BookingDiModule.kt | 4 +- .../features/booking/facade/BookingFacade.kt | 4 +- .../booking/facade/BookingFacadeV1.kt | 4 +- .../booking/routes/BookingRoutingV1.kt | 8 +-- .../user/repository/UserRepository.kt | 23 +++++++ .../office/effective/plugins/Routing.kt | 2 + .../effective/booking/BookingFacadeTest.kt | 18 ++--- 10 files changed, 91 insertions(+), 41 deletions(-) rename effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/{BookingFacadeConverter.kt => BookingDtoModelConverter.kt} (67%) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingResponseDTO.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingResponseDTO.kt index 4e2692137..9794ecc7e 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingResponseDTO.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingResponseDTO.kt @@ -5,7 +5,7 @@ import model.RecurrenceDTO @Serializable data class BookingResponseDTO ( - val owner: UserDTO, + val owner: UserDTO?, val participants: List, val workspace: WorkspaceDTO, val id: String, diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingFacadeConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingDtoModelConverter.kt similarity index 67% rename from effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingFacadeConverter.kt rename to effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingDtoModelConverter.kt index 4a0eb014d..0d69d6f2e 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingFacadeConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingDtoModelConverter.kt @@ -2,9 +2,12 @@ package office.effective.features.booking.converters import model.RecurrenceDTO import office.effective.common.exception.InstanceNotFoundException +import office.effective.common.exception.MissingIdException +import office.effective.common.utils.UuidValidator import office.effective.dto.BookingDTO import office.effective.dto.BookingRequestDTO import office.effective.dto.BookingResponseDTO +import office.effective.dto.UserDTO import office.effective.features.user.converters.UserDTOModelConverter import office.effective.features.user.repository.UserRepository import office.effective.features.workspace.converters.WorkspaceFacadeConverter @@ -18,11 +21,12 @@ import java.time.Instant * * Uses [UserDTOModelConverter] and [WorkspaceFacadeConverter] to convert contained users and workspaces */ -class BookingFacadeConverter( +class BookingDtoModelConverter( private val userConverter: UserDTOModelConverter, private val workspaceConverter: WorkspaceFacadeConverter, private val userRepository: UserRepository, - private val workspaceRepository: WorkspaceRepository + private val workspaceRepository: WorkspaceRepository, + private val uuidValidator: UuidValidator ) { private val logger = LoggerFactory.getLogger(this::class.java) @@ -44,7 +48,8 @@ class BookingFacadeConverter( recurrenceDTO = RecurrenceConverter.modelToDto(booking.recurrence!!) } return BookingDTO( - owner = userConverter.modelToDTO(booking.owner), + owner = booking.owner?.let { userConverter.modelToDTO(it) } + ?: UserDTO("", "", true, "", "", listOf(), "", ""), participants = booking.participants.map { userConverter.modelToDTO(it) }, workspace = workspaceConverter.modelToDto(booking.workspace), id = booking.id.toString(), @@ -57,24 +62,31 @@ class BookingFacadeConverter( /** * Converts [Booking] to [BookingResponseDTO] * + * @throws [MissingIdException] if [booking] doesn't contain an id * @param booking [Booking] to be converted * @return The resulting [BookingResponseDTO] object * @author Daniil Zavyalov, Danil Kiselev */ fun modelToResponseDto(booking: Booking): BookingResponseDTO { logger.trace("Converting booking model to response dto") - var recurrenceDTO : RecurrenceDTO? = null - if(booking.recurrence != null) { - recurrenceDTO = RecurrenceConverter.modelToDto(booking.recurrence!!) + val id = booking.id ?: throw MissingIdException("Booking response must contains an id, but it was missed") + val owner = booking.owner?.let { + owner -> userConverter.modelToDTO(owner) + } + val participants = booking.participants.map { + participant -> userConverter.modelToDTO(participant) + } + val recurrence = booking.recurrence?.let { + recurrenceModel -> RecurrenceConverter.modelToDto(recurrenceModel) } return BookingResponseDTO( - owner = userConverter.modelToDTO(booking.owner), - participants = booking.participants.map { userConverter.modelToDTO(it) }, + owner = owner, + participants = participants, workspace = workspaceConverter.modelToDto(booking.workspace), - id = booking.id.toString(), + id = id, beginBooking = booking.beginBooking.toEpochMilli(), endBooking = booking.endBooking.toEpochMilli(), - recurrence = recurrenceDTO, + recurrence = recurrence, recurringBookingId = booking.recurringBookingId ) } @@ -108,28 +120,31 @@ class BookingFacadeConverter( } /** - * Converts [BookingDTO] to [Booking] + * Converts [BookingDTO] to [Booking]. Users and workspace will be retrieved from database * - * @param bookingDTO [BookingDTO] to be converted + * @param bookingDto [BookingDTO] to be converted + * @param id booking id * @return The resulting [Booking] object + * @throws [InstanceNotFoundException] if user with the given email or + * workspace with the given id doesn't exist in database * @author Daniil Zavyalov, Danil Kiselev */ fun requestDtoToModel(bookingDto: BookingRequestDTO, id: String? = null): Booking { logger.trace("Converting booking response dto to model") - var recurrenceModel : RecurrenceModel? = null - if(bookingDto.recurrence != null) { - recurrenceModel = RecurrenceConverter.dtoToModel(bookingDto.recurrence) + val recurrence = bookingDto.recurrence?.let { + recurrenceDto -> RecurrenceConverter.dtoToModel(recurrenceDto) } val owner: UserModel? = findOwner(bookingDto.ownerEmail) - val participants = findParticipants() + val participants = findParticipants(bookingDto.participantEmails) + val workspace = findWorkspace(bookingDto.workspaceId) return Booking( owner = owner, - participants = bookingDto.participants.map { userConverter.dTOToModel(it) }, - workspace = workspaceConverter.dtoToModel(bookingDto.workspace), + participants = participants, + workspace = workspace, id = id, beginBooking = Instant.ofEpochMilli(bookingDto.beginBooking), endBooking = Instant.ofEpochMilli(bookingDto.endBooking), - recurrence = recurrenceModel, + recurrence = recurrence, ) } @@ -138,10 +153,20 @@ class BookingFacadeConverter( return userRepository.findByEmail(ownerEmail) ?: throw InstanceNotFoundException(UserModel::class, "User with email $ownerEmail not found") } - return null; + return null } private fun findParticipants(participantEmails: List): List { + val users = userRepository.findAllByEmails(participantEmails) + if (users.size < participantEmails.size) { + throw InstanceNotFoundException(UserModel::class, "Participant not found") + } + return users + } + private fun findWorkspace(workspaceUuid: String): Workspace { + return workspaceRepository.findById( + uuidValidator.uuidFromString(workspaceUuid) + ) ?: throw InstanceNotFoundException(Workspace::class, "Workspace with id $workspaceUuid not found") } } \ No newline at end of file diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt index 06f9c1ef5..49c396353 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt @@ -33,7 +33,7 @@ class GoogleCalendarConverter( private val userRepository: UserRepository, private val workspaceConverter: WorkspaceFacadeConverter, private val userConverter: UserDTOModelConverter, - private val bookingConverter: BookingFacadeConverter, + private val bookingConverter: BookingDtoModelConverter, private val verifier: UuidValidator, private val workspaceRepository: WorkspaceRepository ) { diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt index d3529b69b..a1dd57970 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt @@ -1,6 +1,6 @@ package office.effective.features.booking.di -import office.effective.features.booking.converters.BookingFacadeConverter +import office.effective.features.booking.converters.BookingDtoModelConverter import office.effective.features.booking.converters.BookingRepositoryConverter import office.effective.features.booking.facade.BookingFacade import office.effective.features.booking.repository.BookingMeetingRepository @@ -16,6 +16,6 @@ val bookingDiModule = module(createdAtStart = true) { single { BookingRegularRepository(get(), get(), get(), get()) } single { BookingMeetingRepository(get(), get(), get(), get()) } single { BookingService(get(), get(), get(), get()) } - single { BookingFacadeConverter(get(), get(), get(), get()) } + single { BookingDtoModelConverter(get(), get(), get(), get(), get()) } single { BookingFacade(get(), get(), get(), get()) } } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt index ea10c5a0d..0bdbc5b06 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt @@ -5,7 +5,7 @@ import office.effective.common.constants.BookingConstants import office.effective.common.exception.InstanceNotFoundException import office.effective.common.utils.DatabaseTransactionManager import office.effective.common.utils.UuidValidator -import office.effective.features.booking.converters.BookingFacadeConverter +import office.effective.features.booking.converters.BookingDtoModelConverter import office.effective.dto.BookingDTO import office.effective.model.Booking import office.effective.model.Workspace @@ -28,7 +28,7 @@ class BookingFacade( private val bookingService: IBookingService, private val transactionManager: DatabaseTransactionManager, private val uuidValidator: UuidValidator, - private val bookingConverter: BookingFacadeConverter + private val bookingConverter: BookingDtoModelConverter ) { /** diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt index 6d73e0568..d46ac94a9 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt @@ -5,7 +5,7 @@ import office.effective.common.constants.BookingConstants import office.effective.common.exception.InstanceNotFoundException import office.effective.common.utils.DatabaseTransactionManager import office.effective.common.utils.UuidValidator -import office.effective.features.booking.converters.BookingFacadeConverter +import office.effective.features.booking.converters.BookingDtoModelConverter import office.effective.dto.BookingDTO import office.effective.dto.BookingResponseDTO import office.effective.model.Booking @@ -22,7 +22,7 @@ class BookingFacadeV1( private val bookingService: IBookingService, private val transactionManager: DatabaseTransactionManager, private val uuidValidator: UuidValidator, - private val bookingConverter: BookingFacadeConverter + private val bookingConverter: BookingDtoModelConverter ) { /** diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt index 4e06c5b34..f613c8884 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt @@ -29,12 +29,12 @@ fun Route.bookingRoutingV1() { get(SwaggerDocument.returnBookingsV1()) { val userId: String? = call.request.queryParameters["user_id"] val workspaceId: String? = call.request.queryParameters["workspace_id"] - val bookingRangeTo: Long? = call.request.queryParameters["range_to"]?.let { - it.toLongOrNull() + val bookingRangeTo: Long? = call.request.queryParameters["range_to"]?.let { stringRangeTo -> + stringRangeTo.toLongOrNull() ?: throw BadRequestException("range_to can't be parsed to Long") } - call.request.queryParameters["range_from"]?.let { - val bookingRangeFrom: Long = it.toLongOrNull() + call.request.queryParameters["range_from"]?.let { stringRangeFrom -> + val bookingRangeFrom: Long = stringRangeFrom.toLongOrNull() ?: throw BadRequestException("range_from can't be parsed to Long") call.respond(bookingFacade.findAll(userId, workspaceId, bookingRangeTo, bookingRangeFrom)) return@get diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/repository/UserRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/repository/UserRepository.kt index b458df7e4..41dc3b20e 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/repository/UserRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/repository/UserRepository.kt @@ -11,6 +11,7 @@ import org.ktorm.dsl.* import org.ktorm.entity.* import org.slf4j.LoggerFactory import java.util.* +import kotlin.collections.List /** * Perform database queries with users @@ -113,6 +114,28 @@ class UserRepository( return entity?.let { converter.entityToModel(entity, integrations) } } + /** + * Retrieves users models with integrations by emails. + * If a user with one of the specified emails does not exist, that email will be ignored. + * + * @return users with integrations + * @author Daniil Zavyalov + */ + fun findAllByEmails(emails: Collection): List { + logger.debug("[findAllByEmails] retrieving users with emails {}", emails.joinToString()) + val entities: List = db.users.filter { it.email inList emails }.toList() + + val ids : MutableList = mutableListOf() + for (entity in entities) { + ids.add(entity.id) + } + val integrations = findAllIntegrationsByUserIds(ids) + + return entities.map { entity -> + converter.entityToModel(entity, integrations[entity.id]) + } + } + /** * Returns Integration entity by id * @return [IntegrationEntity] diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/Routing.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/Routing.kt index 57f81fce8..3e897cb88 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/Routing.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/Routing.kt @@ -5,6 +5,7 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import office.effective.features.user.routes.userRouting import office.effective.features.booking.routes.bookingRouting +import office.effective.features.booking.routes.bookingRoutingV1 import office.effective.features.notifications.routes.calendarNotificationsRouting import office.effective.features.workspace.routes.workspaceRouting @@ -16,6 +17,7 @@ fun Application.configureRouting() { workspaceRouting() userRouting() bookingRouting() + bookingRoutingV1() calendarNotificationsRouting() } diff --git a/effectiveOfficeBackend/src/test/kotlin/office/effective/booking/BookingFacadeTest.kt b/effectiveOfficeBackend/src/test/kotlin/office/effective/booking/BookingFacadeTest.kt index 6a4cea28a..284e40c30 100644 --- a/effectiveOfficeBackend/src/test/kotlin/office/effective/booking/BookingFacadeTest.kt +++ b/effectiveOfficeBackend/src/test/kotlin/office/effective/booking/BookingFacadeTest.kt @@ -4,7 +4,7 @@ import junit.framework.TestCase.assertEquals import office.effective.common.exception.InstanceNotFoundException import office.effective.common.utils.impl.DatabaseTransactionManagerImpl import office.effective.common.utils.UuidValidator -import office.effective.features.booking.converters.BookingFacadeConverter +import office.effective.features.booking.converters.BookingDtoModelConverter import office.effective.dto.BookingDTO import office.effective.features.booking.facade.BookingFacade import office.effective.features.booking.service.BookingService @@ -35,7 +35,7 @@ class BookingFacadeTest { @Mock private lateinit var transactionManager: DatabaseTransactionManagerImpl @Mock - private lateinit var bookingFacadeConverter: BookingFacadeConverter + private lateinit var bookingDtoModelConverter: BookingDtoModelConverter @Mock private lateinit var user: UserModel @Mock @@ -54,11 +54,11 @@ class BookingFacadeTest { @Before fun setUp() { MockitoAnnotations.openMocks(this) - facade = BookingFacade(service, transactionManager, uuidValidator, bookingFacadeConverter) + facade = BookingFacade(service, transactionManager, uuidValidator, bookingDtoModelConverter) } private fun setUpMockConverter(booking: Booking, bookingDTO: BookingDTO) { - whenever(bookingFacadeConverter.modelToDto(booking)).thenReturn(bookingDTO) + whenever(bookingDtoModelConverter.modelToDto(booking)).thenReturn(bookingDTO) } private fun setUpMockTransactionManager() { @@ -120,7 +120,7 @@ class BookingFacadeTest { setUpMockTransactionManager() whenever(service.findAll(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(existingList) - whenever(bookingFacadeConverter.modelToDto(anyOrNull())).thenReturn(expectedList[0], expectedList[1]) + whenever(bookingDtoModelConverter.modelToDto(anyOrNull())).thenReturn(expectedList[0], expectedList[1]) val result = facade.findAll( "8896abc1-457b-41e4-b80b-2fe7cfb3dbaf", @@ -137,9 +137,9 @@ class BookingFacadeTest { setUpMockTransactionManager() val resultMockDto = mock() val resultMock = mock() - whenever(bookingFacadeConverter.dtoToModel(bookingMockDto)).thenReturn(bookingMock) + whenever(bookingDtoModelConverter.dtoToModel(bookingMockDto)).thenReturn(bookingMock) whenever(service.save(bookingMock)).thenReturn(resultMock) - whenever(bookingFacadeConverter.modelToDto(resultMock)).thenReturn(resultMockDto) + whenever(bookingDtoModelConverter.modelToDto(resultMock)).thenReturn(resultMockDto) val result = facade.post(bookingMockDto) @@ -159,9 +159,9 @@ class BookingFacadeTest { instant.toEpochMilli()) val resultMockDto = mock() val resultMock = mock() - whenever(bookingFacadeConverter.dtoToModel(expectedBookingDTO)).thenReturn(bookingMock) + whenever(bookingDtoModelConverter.dtoToModel(expectedBookingDTO)).thenReturn(bookingMock) whenever(service.update(bookingMock)).thenReturn(resultMock) - whenever(bookingFacadeConverter.modelToDto(resultMock)).thenReturn(expectedBookingDTO) + whenever(bookingDtoModelConverter.modelToDto(resultMock)).thenReturn(expectedBookingDTO) val result = facade.put(expectedBookingDTO) From f74c5d11974f5eec6526e103baaf47e8843f3dd6 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sat, 13 Apr 2024 21:00:50 +0600 Subject: [PATCH 05/26] [~] fix booking service --- .../features/booking/service/BookingService.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt index 7c1a2ad07..d0082bc61 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt @@ -60,14 +60,18 @@ class BookingService( val booking = bookingMeetingRepository.findById(id) ?: bookingRegularRepository.findById(id) ?: return null + val userIds = mutableSetOf() for (participant in booking.participants) { participant.id?.let { userIds.add(it) } } - booking.owner.id?.let { userIds.add(it) } - val integrations = userRepository.findAllIntegrationsByUserIds(userIds) + booking.owner?.id?.let { userIds.add(it) } + booking.workspace.utilities = findUtilities(booking.workspace) - booking.owner.integrations = integrations[booking.owner.id] ?: setOf() + val integrations = userRepository.findAllIntegrationsByUserIds(userIds) + if (booking.owner != null) { + booking.owner?.integrations = integrations[booking.owner?.id] ?: setOf() + } for (participant in booking.participants) { participant.integrations = integrations[participant.id] ?: setOf() } @@ -187,7 +191,7 @@ class BookingService( userIds.add(it) } } - booking.owner.id?.let { + booking.owner?.id?.let { userIds.add(it) } booking.workspace.id?.let { @@ -215,7 +219,9 @@ class BookingService( ): List { for (booking in bookingList) { booking.workspace.utilities = utilities[booking.workspace.id] ?: listOf() - booking.owner.integrations = integrations[booking.owner.id] ?: setOf() + if (booking.owner != null) { + booking.owner?.integrations = integrations[booking.owner?.id] ?: setOf() + } for (participant in booking.participants) { participant.integrations = integrations[participant.id] } From 378ec6cb9bb882499cbe4064b6ff59fb9996c18a Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sat, 13 Apr 2024 21:21:41 +0600 Subject: [PATCH 06/26] [~] fix post and put booking --- .../booking/facade/BookingFacadeV1.kt | 25 +++++++++---------- .../booking/routes/BookingRoutingV1.kt | 11 +++++--- .../routes/swagger/BookingSwaggerV1.kt | 6 +++++ 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt index d46ac94a9..a9e2e2cb8 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt @@ -7,6 +7,7 @@ import office.effective.common.utils.DatabaseTransactionManager import office.effective.common.utils.UuidValidator import office.effective.features.booking.converters.BookingDtoModelConverter import office.effective.dto.BookingDTO +import office.effective.dto.BookingRequestDTO import office.effective.dto.BookingResponseDTO import office.effective.model.Booking import office.effective.model.Workspace @@ -91,15 +92,15 @@ class BookingFacadeV1( /** * Saves a given booking. Use the returned model for further operations * - * @param bookingDTO [BookingDTO] to be saved - * @return saved [BookingDTO] + * @param bookingDTO [BookingRequestDTO] to be saved + * @return saved [BookingResponseDTO] * @author Daniil Zavyalov */ - fun post(bookingDTO: BookingDTO): BookingDTO { - val model = bookingConverter.dtoToModel(bookingDTO) - val dto: BookingDTO = transactionManager.useTransaction({ + fun post(bookingDTO: BookingRequestDTO): BookingResponseDTO { + val model = bookingConverter.requestDtoToModel(bookingDTO) + val dto: BookingResponseDTO = transactionManager.useTransaction({ val savedModel = bookingService.save(model) - bookingConverter.modelToDto(savedModel) + bookingConverter.modelToResponseDto(savedModel) }) return dto } @@ -108,16 +109,14 @@ class BookingFacadeV1( * Updates a given booking. Use the returned model for further operations * * @param bookingDTO changed booking - * @return [BookingDTO] after change saving - * @throws BadRequestException if booking id is null + * @return updated booking * @author Daniil Zavyalov */ - fun put(bookingDTO: BookingDTO): BookingDTO { - if (bookingDTO.id == null) throw BadRequestException("Missing booking id") - val model = bookingConverter.dtoToModel(bookingDTO) - val dto: BookingDTO = transactionManager.useTransaction({ + fun put(bookingDTO: BookingRequestDTO, bookingId: String): BookingResponseDTO { + val model = bookingConverter.requestDtoToModel(bookingDTO, bookingId) + val dto: BookingResponseDTO = transactionManager.useTransaction({ val savedModel = bookingService.update(model) - bookingConverter.modelToDto(savedModel) + bookingConverter.modelToResponseDto(savedModel) }) return dto } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt index f613c8884..bbde38f13 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt @@ -12,6 +12,7 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import office.effective.common.swagger.SwaggerDocument import office.effective.dto.BookingDTO +import office.effective.dto.BookingRequestDTO import office.effective.features.booking.facade.BookingFacadeV1 import office.effective.features.booking.routes.swagger.* import org.koin.core.context.GlobalContext @@ -42,16 +43,18 @@ fun Route.bookingRoutingV1() { call.respond(bookingFacade.findAll(userId, workspaceId, bookingRangeTo)) } post(SwaggerDocument.postBookingV1()) { - val dto = call.receive() + val dto = call.receive() call.response.status(HttpStatusCode.Created) val result = bookingFacade.post(dto) call.respond(result) } - put(SwaggerDocument.putBookingV1()) { - val dto = call.receive() + put("/{id}", SwaggerDocument.putBookingV1()) { + val id: String = call.parameters["id"] + ?: return@put call.respond(HttpStatusCode.BadRequest) + val dto = call.receive() - val result = bookingFacade.put(dto) + val result = bookingFacade.put(dto, id) call.respond(result) } delete("{id}", SwaggerDocument.deleteBookingByIdV1()) { diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt index fa278ff9a..b6995e3b7 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt @@ -203,6 +203,12 @@ fun SwaggerDocument.putBookingV1(): OpenApiRoute.() -> Unit = { "Bookings", bookingExample1 ) } + pathParameter("id") { + description = "Booking id" + example = "p0v9udrhk66cailnigi0qkrji4" + required = true + allowEmptyValue = false + } } response { HttpStatusCode.OK to { From 4174efcd8ca884dcfd80ec065cc5b9febb7c54de Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sat, 13 Apr 2024 22:16:04 +0600 Subject: [PATCH 07/26] [+] add default values for getting bookings time range --- .../common/constants/BookingConstants.kt | 2 ++ .../booking/routes/BookingRoutingV1.kt | 25 ++++++++++++------- .../routes/swagger/BookingSwaggerV1.kt | 8 +++--- .../src/main/resources/application.conf | 1 + 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt index 258162909..75a6ccb0e 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt @@ -18,6 +18,8 @@ object BookingConstants { val WORKSPACE_CALENDAR: String = System.getenv("WORKSPACE_CALENDAR") ?: config.propertyOrNull("calendar.workspaceCalendar")?.getString() ?: throw Exception("Environment and config file does not contain workspace Google calendar id") + val DEFAULT_TIMEZONE_ID: String = config.propertyOrNull("calendar.defaultTimezone")?.getString() + ?: throw Exception("Environment and config file does not contain default timezone id") const val UNTIL_FORMAT = "yyyyMMdd" } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt index bbde38f13..509ec8b5b 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt @@ -10,12 +10,14 @@ import io.ktor.server.plugins.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import office.effective.common.constants.BookingConstants import office.effective.common.swagger.SwaggerDocument -import office.effective.dto.BookingDTO import office.effective.dto.BookingRequestDTO import office.effective.features.booking.facade.BookingFacadeV1 import office.effective.features.booking.routes.swagger.* import org.koin.core.context.GlobalContext +import java.time.LocalDate +import java.time.ZoneId fun Route.bookingRoutingV1() { route("/v1/bookings") { @@ -28,19 +30,24 @@ fun Route.bookingRoutingV1() { } get(SwaggerDocument.returnBookingsV1()) { + val todayEpoch = LocalDate.now() + .atStartOfDay(ZoneId.of(BookingConstants.DEFAULT_TIMEZONE_ID)) + .toInstant() + .toEpochMilli() + val endOfDayEpoch = todayEpoch + 1000*60*60*24 + val userId: String? = call.request.queryParameters["user_id"] val workspaceId: String? = call.request.queryParameters["workspace_id"] - val bookingRangeTo: Long? = call.request.queryParameters["range_to"]?.let { stringRangeTo -> + val bookingRangeTo: Long = call.request.queryParameters["range_to"]?.let { stringRangeTo -> stringRangeTo.toLongOrNull() ?: throw BadRequestException("range_to can't be parsed to Long") - } - call.request.queryParameters["range_from"]?.let { stringRangeFrom -> - val bookingRangeFrom: Long = stringRangeFrom.toLongOrNull() + } ?: todayEpoch + val bookingRangeFrom: Long = call.request.queryParameters["range_from"]?.let { stringRangeFrom -> + stringRangeFrom.toLongOrNull() ?: throw BadRequestException("range_from can't be parsed to Long") - call.respond(bookingFacade.findAll(userId, workspaceId, bookingRangeTo, bookingRangeFrom)) - return@get - } - call.respond(bookingFacade.findAll(userId, workspaceId, bookingRangeTo)) + } ?: endOfDayEpoch + + call.respond(bookingFacade.findAll(userId, workspaceId, bookingRangeTo, bookingRangeFrom)) } post(SwaggerDocument.postBookingV1()) { val dto = call.receive() diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt index b6995e3b7..8b3323890 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt @@ -69,17 +69,15 @@ fun SwaggerDocument.returnBookingsV1(): OpenApiRoute.() -> Unit = { } queryParameter("range_from") { description = "Lower bound (exclusive) for a endBooking to filter by. Should be lover than range_to. " + - "Default value: ${BookingConstants.MIN_SEARCH_START_TIME} " + - "(${Instant.ofEpochMilli(BookingConstants.MIN_SEARCH_START_TIME)}). " + - "Old Google calendar events may not appear correctly in the system and cause unexpected exceptions" + "The default value is the beginning of today." example = 1692927200000 required = false allowEmptyValue = false } queryParameter("range_to") { - description = "Upper bound (exclusive) for a beginBooking to filter by. Optional. " + - "Should be greater than range_from." + description = "Upper bound (exclusive) for a beginBooking to filter by. " + + "The default value is the end of today. Should be greater than range_from." example = 1697027200000 required = false allowEmptyValue = false diff --git a/effectiveOfficeBackend/src/main/resources/application.conf b/effectiveOfficeBackend/src/main/resources/application.conf index 9eec39aa4..6f8e33d9f 100644 --- a/effectiveOfficeBackend/src/main/resources/application.conf +++ b/effectiveOfficeBackend/src/main/resources/application.conf @@ -10,6 +10,7 @@ database { } calendar { minTime = "1692727200000" + defaultTimezone = "Asia/Omsk" defaultCalendar = "effective.office@effective.band" workspaceCalendar = "c_46707d19c716de0d5d28b52082edfeb03376269e7da5fea78e43fcb15afda57e@group.calendar.google.com" } From f8781becc82a4c7ddb6839e6db0b8b0e68d794a3 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sun, 14 Apr 2024 18:46:01 +0600 Subject: [PATCH 08/26] [~] fix --- .../booking/converters/BookingRepositoryConverter.kt | 12 +++++++++++- .../booking/repository/BookingRegularRepository.kt | 6 ------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingRepositoryConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingRepositoryConverter.kt index fce520dea..8f1410fd9 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingRepositoryConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/BookingRepositoryConverter.kt @@ -6,6 +6,7 @@ import office.effective.common.utils.UuidValidator import office.effective.features.booking.repository.WorkspaceBookingEntity import office.effective.features.user.converters.UserModelEntityConverter import office.effective.features.user.repository.UserEntity +import office.effective.features.user.repository.UsersTagEntity import office.effective.features.user.repository.users import office.effective.features.workspace.converters.WorkspaceRepositoryConverter import office.effective.features.workspace.repository.WorkspaceEntity @@ -74,7 +75,16 @@ class BookingRepositoryConverter(private val database: Database, fun modelToEntity(bookingModel: Booking): WorkspaceBookingEntity { logger.trace("Converting booking model to entity") return WorkspaceBookingEntity { - owner = findOwnerEntity(bookingModel.owner) + owner = bookingModel.owner?.let { findOwnerEntity(it) } + ?: UserEntity { + id = UUID.randomUUID() + fullName = "" + tag = UsersTagEntity {} + active = false + role = "" + avatarURL = "" + email = "" + } workspace = findWorkspaceEntity(bookingModel.workspace) id = bookingModel.id?.let { uuidValidator.uuidFromString(it) } ?: throw MissingIdException("Booking model doesn't have an id") diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt index 4619d6708..00cc2a838 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt @@ -242,12 +242,6 @@ class BookingRegularRepository( override fun save(booking: Booking): Booking { logger.debug("[save] saving booking of workspace with id {}", booking.workspace.id) val workspaceId = booking.workspace.id ?: throw MissingIdException("Missing booking workspace id") - val userId = booking.owner.id ?: throw MissingIdException("Missing booking owner id") - if (!userRepository.existsById(userId)) { - throw InstanceNotFoundException( - WorkspaceEntity::class, "User with id $workspaceId not wound" - ) - } val event = googleCalendarConverter.toGoogleWorkspaceEvent(booking) logger.trace("[save] booking to save: {}", event) From b399754d65476ed557c9f206ca68d22b0b5c9cb5 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sun, 14 Apr 2024 22:16:44 +0600 Subject: [PATCH 09/26] [~] add workspaces with bookings request --- .../common/constants/BookingConstants.kt | 1 + .../office/effective/dto/WorkspaceDTO.kt | 3 +- .../features/booking/di/BookingDiModule.kt | 2 + .../booking/routes/BookingRoutingV1.kt | 2 +- .../workspace/DI/WorkspaceDiModule.kt | 2 + .../converters/WorkspaceFacadeConverter.kt | 11 +- .../workspace/facade/WorkspaceFacadeV1.kt | 141 +++++++++++++ .../workspace/routes/WorkspaceRoutingV1.kt | 70 +++++++ .../routes/swagger/WorkspaceSwagger.kt | 7 - .../routes/swagger/WorkspaceSwaggerV1.kt | 192 ++++++++++++++++++ .../workspace/service/WorkspaceService.kt | 11 +- 11 files changed, 426 insertions(+), 16 deletions(-) create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwaggerV1.kt diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt index 75a6ccb0e..bb0f053ab 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt @@ -12,6 +12,7 @@ object BookingConstants { */ val MIN_SEARCH_START_TIME = config.propertyOrNull("calendar.minTime")?.getString()?.toLong() ?: throw Exception("Config file does not contain minimum time") + val MAX_TIMESTAMP = 2147483647000L val DEFAULT_CALENDAR: String = System.getenv("DEFAULT_CALENDAR") ?: config.propertyOrNull("calendar.defaultCalendar")?.getString() ?: throw Exception("Environment and config file does not contain Google default calendar id") diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/WorkspaceDTO.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/WorkspaceDTO.kt index 1b718668e..4f1dc0256 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/WorkspaceDTO.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/WorkspaceDTO.kt @@ -8,5 +8,6 @@ data class WorkspaceDTO( val name: String, val utilities: List, val zone: WorkspaceZoneDTO? = null, - val tag: String + val tag: String, + val bookings: List? = null ) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt index a1dd57970..d9d144433 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt @@ -6,6 +6,7 @@ import office.effective.features.booking.facade.BookingFacade import office.effective.features.booking.repository.BookingMeetingRepository import office.effective.features.booking.service.BookingService import office.effective.features.booking.converters.GoogleCalendarConverter +import office.effective.features.booking.facade.BookingFacadeV1 import office.effective.features.booking.repository.BookingRegularRepository import office.effective.serviceapi.IBookingService import org.koin.dsl.module @@ -18,4 +19,5 @@ val bookingDiModule = module(createdAtStart = true) { single { BookingService(get(), get(), get(), get()) } single { BookingDtoModelConverter(get(), get(), get(), get(), get()) } single { BookingFacade(get(), get(), get(), get()) } + single { BookingFacadeV1(get(), get(), get(), get()) } } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt index 509ec8b5b..2e769f29d 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt @@ -20,7 +20,7 @@ import java.time.LocalDate import java.time.ZoneId fun Route.bookingRoutingV1() { - route("/v1/bookings") { + route("/api/v1/bookings") { val bookingFacade: BookingFacadeV1 = GlobalContext.get().get() get("/{id}", SwaggerDocument.returnBookingByIdV1()) { diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/DI/WorkspaceDiModule.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/DI/WorkspaceDiModule.kt index 8a007968f..ead428bf3 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/DI/WorkspaceDiModule.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/DI/WorkspaceDiModule.kt @@ -3,6 +3,7 @@ package office.effective.features.workspace.DI import office.effective.features.workspace.converters.WorkspaceFacadeConverter import office.effective.features.workspace.converters.WorkspaceRepositoryConverter import office.effective.features.workspace.facade.WorkspaceFacade +import office.effective.features.workspace.facade.WorkspaceFacadeV1 import office.effective.features.workspace.repository.WorkspaceRepository import office.effective.features.workspace.service.WorkspaceService import office.effective.serviceapi.IWorkspaceService @@ -15,4 +16,5 @@ val workspaceDiModule = module(createdAtStart = true) { single { get() } single { WorkspaceFacadeConverter(get()) } single { WorkspaceFacade(get(), get(), get(), get()) } + single { WorkspaceFacadeV1(get(), get(), get(), get(), get()) } } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/converters/WorkspaceFacadeConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/converters/WorkspaceFacadeConverter.kt index 2a7e9a7ff..240f214d2 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/converters/WorkspaceFacadeConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/converters/WorkspaceFacadeConverter.kt @@ -1,6 +1,7 @@ package office.effective.features.workspace.converters import office.effective.common.utils.UuidValidator +import office.effective.dto.BookingResponseDTO import office.effective.dto.UtilityDTO import office.effective.dto.WorkspaceDTO import office.effective.dto.WorkspaceZoneDTO @@ -22,13 +23,19 @@ class WorkspaceFacadeConverter(private val uuidValidator: UuidValidator) { * Converts [Workspace] with [WorkspaceZone] and [Utilities][Utility] * to [WorkspaceDTO] with [WorkspaceZoneDTO] and [UtilityDTO]s * @param model The [Workspace] object to be converted + * @param bookings bookings of this workspace * @return The resulting [WorkspaceDTO] object * @author Daniil Zavyalov */ - fun modelToDto(model: Workspace): WorkspaceDTO { + fun modelToDto(model: Workspace, bookings: List? = null): WorkspaceDTO { val utilities = model.utilities.map { utilityModelToDto(it) } return WorkspaceDTO( - model.id.toString(), model.name, utilities, model.zone?.let { zoneModelToDto(it) }, model.tag + id = model.id.toString(), + name = model.name, + utilities = utilities, + zone = model.zone?.let { zoneModelToDto(it) }, + tag = model.tag, + bookings = bookings ) } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt new file mode 100644 index 000000000..098abcf38 --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt @@ -0,0 +1,141 @@ +package office.effective.features.workspace.facade + +import office.effective.common.constants.BookingConstants +import office.effective.common.exception.InstanceNotFoundException +import office.effective.common.exception.ValidationException +import office.effective.common.utils.DatabaseTransactionManager +import office.effective.common.utils.UuidValidator +import office.effective.features.workspace.converters.WorkspaceFacadeConverter +import office.effective.dto.WorkspaceDTO +import office.effective.dto.WorkspaceZoneDTO +import office.effective.features.booking.facade.BookingFacadeV1 +import office.effective.features.booking.service.BookingService +import office.effective.model.Workspace +import office.effective.serviceapi.IWorkspaceService +import java.time.Instant + +/** + * Class used in routes to handle workspaces requests. + * Provides business transaction, data conversion and validation. + * + * In case of an error, the database transaction will be rolled back. + */ +class WorkspaceFacadeV1( + private val service: IWorkspaceService, + private val converter: WorkspaceFacadeConverter, + private val transactionManager: DatabaseTransactionManager, + private val uuidValidator: UuidValidator, + private val bookingFacade: BookingFacadeV1 +) { + + /** + * Retrieves a [WorkspaceDTO] by its id + * + * @param id id of requested workspace. Should be valid UUID + * @return [WorkspaceDTO] with the given [id] + * @throws InstanceNotFoundException if workspace with the given id doesn't exist in database + * @author Daniil Zavyalov + */ + fun findById(id: String): WorkspaceDTO { + val uuid = uuidValidator.uuidFromString(id) + + val workspaceDTO: WorkspaceDTO = transactionManager.useTransaction({ + val workspace = service.findById(uuid) + ?: throw InstanceNotFoundException(Workspace::class, "Workspace with id $id not found", uuid) + workspace.let { converter.modelToDto(it) } + }) + + return workspaceDTO + } + + /** + * Returns all [WorkspaceDTO] with the given tag + * + * @param tag tag name of requested workspaces + * @return List of [WorkspaceDTO] with the given [tag] + * @author Daniil Zavyalov + */ + fun findAllByTag( + tag: String, + withBookingsFrom: Long? = null, + withBookingsUntil: Long? = null + ): List { + if (withBookingsFrom != null && withBookingsUntil != null) { + if (withBookingsFrom < 0L || withBookingsFrom >= BookingConstants.MAX_TIMESTAMP) + throw ValidationException("with_bookings_from should be non-negative and greater than timestamp max value") + else if (withBookingsUntil < 0L || withBookingsUntil >= BookingConstants.MAX_TIMESTAMP) + throw ValidationException("with_bookings_until should be non-negative and less than timestamp max value") + else if (withBookingsUntil <= withBookingsFrom) + throw ValidationException( + "with_bookings_until (${withBookingsUntil}) should be greater than with_bookings_from (${withBookingsFrom})") + + return findAllByTag(tag).map { workspace -> + val bookings = bookingFacade.findAll( + userId = null, + workspaceId = workspace.id.toString(), + bookingRangeFrom = withBookingsFrom, + bookingRangeTo = withBookingsUntil + ) + converter.modelToDto(workspace, bookings) + } + } + return findAllByTag(tag).map { workspace -> + converter.modelToDto(workspace) + } + } + + /** + * Returns all [Workspace] with the given tag + * + * @param tag tag name of requested workspaces + * @return List of [Workspace] with the given [tag] + * @author Daniil Zavyalov + */ + private fun findAllByTag(tag: String): List { + return transactionManager.useTransaction({ + service.findAllByTag(tag) + }) + } + + /** + * Returns all [Workspace]s with the given tag which are free during the given period + * + * @param tag tag name of requested workspaces + * @param beginTimestamp period start time + * @param endTimestamp period end time + * @return List of [WorkspaceDTO] with the given [tag] + * @throws ValidationException if begin or end timestamp less than 0, greater than max timestamp + * or if end timestamp less than or equal to begin timestamp + * @author Daniil Zavyalov + */ + fun findAllFreeByPeriod(tag: String, beginTimestamp: Long, endTimestamp: Long): List { + if (beginTimestamp < 0L || beginTimestamp >= BookingConstants.MAX_TIMESTAMP) + throw ValidationException("Begin timestamp should be non-negative and less than timestamp max value") + else if (endTimestamp < 0L || endTimestamp >= BookingConstants.MAX_TIMESTAMP) + throw ValidationException("End timestamp should be non-negative and less than timestamp max value") + else if (endTimestamp <= beginTimestamp) + throw ValidationException( + "End timestamp (${endTimestamp}) should be greater than begin timestamp (${beginTimestamp})") + + return transactionManager.useTransaction({ + val modelList = service.findAllFreeByPeriod( + tag, + Instant.ofEpochMilli(beginTimestamp), + Instant.ofEpochMilli(endTimestamp) + ) + modelList.map { converter.modelToDto(it) } + }) + } + + /** + * Returns all workspace zones + * + * @return List of all [WorkspaceZoneDTO] + * @author Daniil Zavyalov + */ + fun findAllZones(): List { + return transactionManager.useTransaction({ + service.findAllZones().map { converter.zoneModelToDto(it) } + }) + } +} \ No newline at end of file diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt new file mode 100644 index 000000000..5b0228774 --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt @@ -0,0 +1,70 @@ +package office.effective.features.workspace.routes + +import io.github.smiley4.ktorswaggerui.dsl.get +import io.github.smiley4.ktorswaggerui.dsl.route +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import office.effective.common.constants.BookingConstants +import office.effective.common.swagger.SwaggerDocument +import office.effective.features.workspace.facade.WorkspaceFacadeV1 +import office.effective.features.workspace.routes.swagger.* +import org.koin.core.context.GlobalContext +import java.time.LocalDate +import java.time.ZoneId + +fun Route.workspaceRoutingV1() { + route("/api/v1/workspaces") { + val facade: WorkspaceFacadeV1 = GlobalContext.get().get() + + get("/{id}", SwaggerDocument.returnWorkspaceByIdV1()) { + val id: String = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest) + call.respond(facade.findById(id)) + } + get(SwaggerDocument.returnWorkspaceByTagV1()) { + val tag: String = call.request.queryParameters["workspace_tag"] + ?: return@get call.respond(HttpStatusCode.BadRequest) + val freeFromString: String? = call.request.queryParameters["free_from"] + val freeUntilString: String? = call.request.queryParameters["free_until"] + val withBookingsString: String? = call.request.queryParameters["with_bookings"] + val withBookingsFromString: String? = call.request.queryParameters["with_bookings_from"] + val withBookingsUntilString: String? = call.request.queryParameters["with_bookings_until"] + + if (freeFromString == null && freeUntilString == null) { + var withBookingsFrom: Long? = null + var withBookingsUntil: Long? = null + val withBookings = withBookingsString?.let { paramString -> + paramString.toBooleanStrictOrNull() + ?: throw BadRequestException("with_bookings can't be parsed to boolean") + } ?: false + if (withBookings || withBookingsFromString != null || withBookingsUntilString != null) { + val todayEpoch = LocalDate.now() + .atStartOfDay(ZoneId.of(BookingConstants.DEFAULT_TIMEZONE_ID)) + .toInstant() + .toEpochMilli() + val endOfDayEpoch = todayEpoch + 1000*60*60*24 + withBookingsFrom = withBookingsFromString?.let { paramString -> + paramString.toLongOrNull() + ?: throw BadRequestException("with_bookings_from can't be parsed to Long") + } ?: todayEpoch + withBookingsUntil = withBookingsUntilString?.let { paramString -> + paramString.toLongOrNull() + ?: throw BadRequestException("with_bookings_until can't be parsed to Long") + } ?: endOfDayEpoch + } + call.respond(facade.findAllByTag(tag, withBookingsFrom, withBookingsUntil)) + } else { + val freeFrom: Long = freeFromString?.toLongOrNull() + ?: throw BadRequestException("free_from not specified or invalid") + val freeUntil: Long = freeUntilString?.toLongOrNull() + ?: throw BadRequestException("free_until not specified or invalid") + call.respond(facade.findAllFreeByPeriod(tag, freeFrom, freeUntil)) + } + } + get("/zones", SwaggerDocument.returnAllZonesV1()) { + call.respond(facade.findAllZones()) + } + } +} diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwagger.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwagger.kt index 214468251..2a3ba4662 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwagger.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwagger.kt @@ -157,10 +157,3 @@ private val zoneExample1 = WorkspaceZoneDTO("3ca26fe0-f837-4939-b586-dd4195d2a50 * @suppress */ private val zoneExample2 = WorkspaceZoneDTO("6cb3c60d-3c29-4a45-80e6-fac14fb0569b","Sirius") - -/** - * @suppress - */ -enum class WorkspaceTag(val tagName: String) { - meeting("meeting"), regular("regular") -} diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwaggerV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwaggerV1.kt new file mode 100644 index 000000000..712bbb4a1 --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwaggerV1.kt @@ -0,0 +1,192 @@ +/** + * @suppress + */ +package office.effective.features.workspace.routes.swagger + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.ktor.http.* +import office.effective.common.swagger.SwaggerDocument +import office.effective.dto.UtilityDTO +import office.effective.dto.WorkspaceDTO +import office.effective.dto.WorkspaceZoneDTO + +/** + * @suppress + */ +fun SwaggerDocument.returnWorkspaceByIdV1(): OpenApiRoute.() -> Unit = { + description = "Return workspace by id" + tags = listOf("workspaces") + request { + pathParameter("id") { + description = "Workspace id" + example = "2561471e-2bc6-11ee-be56-0242ac120002" + required = true + allowEmptyValue = false + } + } + response { + HttpStatusCode.OK to { + description = "Returns workspace found by id" + body { + example( + "Workspaces", WorkspaceDTO( + id = "2561471e-2bc6-11ee-be56-0242ac120002", name = "Sun", tag = "meeting", + utilities = listOf( + UtilityDTO( + id = "50d89406-2bc6-11ee-be56-0242ac120002", + name = "Sockets", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 8 + ), UtilityDTO( + id = "a62a86c6-2bc6-11ee-be56-0242ac120002", + name = "Projectors", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 1 + ) + ) + ) + ) {} + } + } + HttpStatusCode.BadRequest to { + description = "Bad request" + } + HttpStatusCode.NotFound to { + description = "Workspace with this id was not found" + } + } +} + +/** + * @suppress + */ +fun SwaggerDocument.returnWorkspaceByTagV1(): OpenApiRoute.() -> Unit = { + description = "Return all workspaces by tag" + tags = listOf("workspaces") + request { + queryParameter("workspace_tag") { + description = "Workspace tag" + example = "meeting" + required = true + allowEmptyValue = false + } + queryParameter("free_from") { + description = "Timestamp from which the workspace should be free." + example = 1691591501000L + required = false + allowEmptyValue = false + } + queryParameter("free_until") { + description = "Timestamp before which the workspace should be free. Should be greater than free_from." + example = 1691591578000L + required = false + allowEmptyValue = false + } + queryParameter("with_bookings") { + description = "Specify this parameter to get workspaces with their bookings for today. Optional. " + + "Can't be specified together with free_from and free_until. " + + "Requesting workspaces with their bookings significantly increases the response time." + example = true + required = false + allowEmptyValue = false + } + queryParameter("with_bookings_from") { + description = "Specify this parameter to get workspaces with their bookings for a certain period of time. " + + "The default value is the beginning of today. " + + "Should be greater than with_bookings_from. Can't be specified together with free_from and free_until. " + + "Requesting workspaces with their bookings significantly increases the response time." + example = 1691591501000L + required = false + allowEmptyValue = false + } + queryParameter("with_bookings_until") { + description = "Specify this parameter to get workspaces with their bookings for a certain period of time. " + + "The default value is the end of today. " + + "Should be greater than with_bookings_from. Can't be specified together with free_from and free_until. " + + "Requesting workspaces with their bookings significantly increases the response time." + example = 1691591578000L + required = false + allowEmptyValue = false + } + } + response { + HttpStatusCode.OK to { + description = "Returns all workspaces found by tag" + body> { + example( + "Workspace", listOf( + WorkspaceDTO( + id = "2561471e-2bc6-11ee-be56-0242ac120002", name = "Sun", tag = "meeting", + utilities = listOf( + UtilityDTO( + id = "50d89406-2bc6-11ee-be56-0242ac120002", + name = "Sockets", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 8 + ), UtilityDTO( + id = "a62a86c6-2bc6-11ee-be56-0242ac120002", + name = "Projectors", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 1 + ) + ) + ), WorkspaceDTO( + id = "2561471e-2bc6-11ee-be56-0242ac120002", name = "Moon", tag = "meeting", + utilities = listOf( + UtilityDTO( + id = "50d89406-2bc6-11ee-be56-0242ac120002", + name = "Sockets", + iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", + count = 5 + ), + ) + ) + ) + ) {} + } + } + HttpStatusCode.BadRequest to { + description = "Tag shouldn't be null, free_from " + + "and free_until should be numbers from 0 to 2147483647000 (max timestamp)" + } + HttpStatusCode.NotFound to { + description = "Provided tag doesn't exist" + } + } +} + +/** + * @suppress + */ +fun SwaggerDocument.returnAllZonesV1(): OpenApiRoute.() -> Unit = { + description = "Returns all workspace zones" + tags = listOf("workspaces") + response { + HttpStatusCode.OK to { + description = "Returns all workspaces found by tag" + body> { + example( + "Zones", listOf( + zoneExample1, zoneExample2 + ) + ) {} + } + } + } +} + +/** + * @suppress + */ +private val zoneExample1 = WorkspaceZoneDTO("3ca26fe0-f837-4939-b586-dd4195d2a504","Cassiopeia") +/** + * @suppress + */ +private val zoneExample2 = WorkspaceZoneDTO("6cb3c60d-3c29-4a45-80e6-fac14fb0569b","Sirius") + +/** + * @suppress + */ +enum class WorkspaceTag(val tagName: String) { + meeting("meeting"), regular("regular") +} diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/service/WorkspaceService.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/service/WorkspaceService.kt index ea56ebadf..424cc78ee 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/service/WorkspaceService.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/service/WorkspaceService.kt @@ -1,5 +1,6 @@ package office.effective.features.workspace.service +import office.effective.features.booking.service.BookingService import office.effective.features.workspace.repository.WorkspaceRepository import office.effective.model.Workspace import office.effective.model.WorkspaceZone @@ -10,7 +11,7 @@ import java.util.UUID /** * Class that implements the [IWorkspaceService] methods */ -class WorkspaceService(private val repository: WorkspaceRepository): IWorkspaceService { +class WorkspaceService(private val workspaceRepository: WorkspaceRepository): IWorkspaceService { /** * Retrieves a Workspace model by its id @@ -20,7 +21,7 @@ class WorkspaceService(private val repository: WorkspaceRepository): IWorkspaceS * @author Daniil Zavyalov */ override fun findById(id: UUID): Workspace? { - return repository.findById(id) + return workspaceRepository.findById(id) } /** @@ -31,7 +32,7 @@ class WorkspaceService(private val repository: WorkspaceRepository): IWorkspaceS * @author Daniil Zavyalov */ override fun findAllByTag(tag: String): List { - return repository.findAllByTag(tag) + return workspaceRepository.findAllByTag(tag) } /** @@ -44,7 +45,7 @@ class WorkspaceService(private val repository: WorkspaceRepository): IWorkspaceS * @author Daniil Zavyalov */ override fun findAllFreeByPeriod(tag: String, beginTimestamp: Instant, endTimestamp: Instant): List { - return repository.findAllFreeByPeriod(tag, beginTimestamp, endTimestamp) + return workspaceRepository.findAllFreeByPeriod(tag, beginTimestamp, endTimestamp) } /** @@ -54,6 +55,6 @@ class WorkspaceService(private val repository: WorkspaceRepository): IWorkspaceS * @author Daniil Zavyalov */ override fun findAllZones(): List { - return repository.findAllZones() + return workspaceRepository.findAllZones() } } \ No newline at end of file From e3c407c7fbad55308f4dcc4116f1e51a1e32fdff Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sun, 14 Apr 2024 22:28:50 +0600 Subject: [PATCH 10/26] [+] add api version to users requests --- .../features/user/di/userDIModule.kt | 4 +- .../facade/{UserFacade.kt => UserFacadeV1.kt} | 2 +- .../features/user/routes/UserRouting.kt | 16 +++---- .../features/user/routes/UserRoutingV1.kt | 47 +++++++++++++++++++ .../{UserSwagger.kt => UserSwaggerV1.kt} | 6 +-- .../office/effective/plugins/Routing.kt | 6 ++- 6 files changed, 66 insertions(+), 15 deletions(-) rename effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/facade/{UserFacade.kt => UserFacadeV1.kt} (99%) create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/UserRoutingV1.kt rename effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/{UserSwagger.kt => UserSwaggerV1.kt} (97%) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/di/userDIModule.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/di/userDIModule.kt index 30874450b..ee14ff1ba 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/di/userDIModule.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/di/userDIModule.kt @@ -4,7 +4,7 @@ import office.effective.features.user.converters.IntegrationDTOModelConverter import office.effective.features.user.converters.IntegrationModelEntityConverter import office.effective.features.user.converters.UserDTOModelConverter import office.effective.features.user.converters.UserModelEntityConverter -import office.effective.features.user.facade.UserFacade +import office.effective.features.user.facade.UserFacadeV1 import office.effective.features.user.repository.UserRepository import office.effective.serviceapi.IUserService import office.effective.features.user.service.UserService @@ -16,6 +16,6 @@ val userDIModule = module(createdAtStart = true) { single { UserModelEntityConverter() } single { UserDTOModelConverter(get(), get(), get()) } single { UserRepository(get(), get(), get()) } - single { UserFacade(get(), get(), get()) } + single { UserFacadeV1(get(), get(), get()) } single { IntegrationDTOModelConverter(get()) } } \ No newline at end of file diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/facade/UserFacade.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/facade/UserFacadeV1.kt similarity index 99% rename from effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/facade/UserFacade.kt rename to effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/facade/UserFacadeV1.kt index ceb3f023a..d80775b62 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/facade/UserFacade.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/facade/UserFacadeV1.kt @@ -11,7 +11,7 @@ import office.effective.model.UserModel * Class used in routes to handle user-related requests. * Provides business transaction, data conversion and validation. * */ -class UserFacade( +class UserFacadeV1( private val service: IUserService, private val converterDTO: UserDTOModelConverter, private val transactionManager: DatabaseTransactionManager diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/UserRouting.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/UserRouting.kt index 1cb5ab726..8504742bf 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/UserRouting.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/UserRouting.kt @@ -10,17 +10,17 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import office.effective.common.swagger.SwaggerDocument import office.effective.dto.UserDTO -import office.effective.features.user.facade.UserFacade -import office.effective.features.user.routes.swagger.updateUser -import office.effective.features.user.routes.swagger.returnUserById -import office.effective.features.user.routes.swagger.returnUsers +import office.effective.features.user.facade.UserFacadeV1 +import office.effective.features.user.routes.swagger.updateUserV1 +import office.effective.features.user.routes.swagger.returnUserByIdV1 +import office.effective.features.user.routes.swagger.returnUsersV1 import org.koin.core.context.GlobalContext fun Route.userRouting() { - val facade: UserFacade = GlobalContext.get().get() + val facade: UserFacadeV1 = GlobalContext.get().get() route("users", {}) { - get(SwaggerDocument.returnUsers()) { + get(SwaggerDocument.returnUsersV1()) { val tag: String? = call.request.queryParameters["user_tag"] val email: String? = call.request.queryParameters["email"] @@ -34,12 +34,12 @@ fun Route.userRouting() { else -> call.respond(facade.getUsers()) } } - get("/{user_id}", SwaggerDocument.returnUserById()) { + get("/{user_id}", SwaggerDocument.returnUserByIdV1()) { val userId: String = call.parameters["user_id"] ?: return@get call.respond(HttpStatusCode.BadRequest) val user = facade.getUserById(userId) call.respond(user) } - put("/{user_id}", SwaggerDocument.updateUser()) { + put("/{user_id}", SwaggerDocument.updateUserV1()) { val user: UserDTO = call.receive() call.respond(facade.updateUser(user)) } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/UserRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/UserRoutingV1.kt new file mode 100644 index 000000000..c786a7e8c --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/UserRoutingV1.kt @@ -0,0 +1,47 @@ +package office.effective.features.user.routes + +import io.github.smiley4.ktorswaggerui.dsl.get +import io.github.smiley4.ktorswaggerui.dsl.put +import io.github.smiley4.ktorswaggerui.dsl.route +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import office.effective.common.swagger.SwaggerDocument +import office.effective.dto.UserDTO +import office.effective.features.user.facade.UserFacadeV1 +import office.effective.features.user.routes.swagger.updateUserV1 +import office.effective.features.user.routes.swagger.returnUserByIdV1 +import office.effective.features.user.routes.swagger.returnUsersV1 +import org.koin.core.context.GlobalContext + +fun Route.userRoutingV1() { + val facade: UserFacadeV1 = GlobalContext.get().get() + + route("/api/v1/users", {}) { + get(SwaggerDocument.returnUsersV1()) { + val tag: String? = call.request.queryParameters["user_tag"] + val email: String? = call.request.queryParameters["email"] + + when { + (email != null && tag != null) -> { + call.response.status(HttpStatusCode.BadRequest) + call.respondText("email and tag are mutually exclusive parameters") + } + (email != null) -> call.respond(facade.getUserByEmail(email)) + (tag != null) -> call.respond(facade.getUsersByTag(tag)) + else -> call.respond(facade.getUsers()) + } + } + get("/{user_id}", SwaggerDocument.returnUserByIdV1()) { + val userId: String = call.parameters["user_id"] ?: return@get call.respond(HttpStatusCode.BadRequest) + val user = facade.getUserById(userId) + call.respond(user) + } + put("/{user_id}", SwaggerDocument.updateUserV1()) { + val user: UserDTO = call.receive() + call.respond(facade.updateUser(user)) + } + } +} \ No newline at end of file diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/UserSwagger.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/UserSwaggerV1.kt similarity index 97% rename from effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/UserSwagger.kt rename to effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/UserSwaggerV1.kt index d60cdf29f..b4bfea827 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/UserSwagger.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/UserSwaggerV1.kt @@ -12,7 +12,7 @@ import office.effective.dto.UserDTO /** * @suppress */ -fun SwaggerDocument.returnUsers(): OpenApiRoute.() -> Unit = { +fun SwaggerDocument.returnUsersV1(): OpenApiRoute.() -> Unit = { description = "Return all users, all users by tag or one user by email" tags = listOf("users") request{ @@ -117,7 +117,7 @@ fun SwaggerDocument.returnUsers(): OpenApiRoute.() -> Unit = { /** * @suppress */ -fun SwaggerDocument.returnUserById(): OpenApiRoute.() -> Unit = { +fun SwaggerDocument.returnUserByIdV1(): OpenApiRoute.() -> Unit = { description = "Return user by id" tags = listOf("users") request { @@ -170,7 +170,7 @@ fun SwaggerDocument.returnUserById(): OpenApiRoute.() -> Unit = { /** * @suppress */ -fun SwaggerDocument.updateUser(): OpenApiRoute.() -> Unit = { +fun SwaggerDocument.updateUserV1(): OpenApiRoute.() -> Unit = { description = "Changes user by id" tags = listOf("users") request { diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/Routing.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/Routing.kt index 3e897cb88..04fbd678a 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/Routing.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/Routing.kt @@ -7,17 +7,21 @@ import office.effective.features.user.routes.userRouting import office.effective.features.booking.routes.bookingRouting import office.effective.features.booking.routes.bookingRoutingV1 import office.effective.features.notifications.routes.calendarNotificationsRouting +import office.effective.features.user.routes.userRoutingV1 import office.effective.features.workspace.routes.workspaceRouting +import office.effective.features.workspace.routes.workspaceRoutingV1 fun Application.configureRouting() { routing { get("/") { call.respondText("Hello World!") } + bookingRoutingV1() + workspaceRoutingV1() + userRoutingV1() workspaceRouting() userRouting() bookingRouting() - bookingRoutingV1() calendarNotificationsRouting() } From 68a3e88ed58f347bf1a76fa3837d8fd4f88aa661 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Mon, 15 Apr 2024 14:31:40 +0600 Subject: [PATCH 11/26] [~] fix event updating --- .../repository/BookingMeetingRepository.kt | 130 +++++++++++------- .../booking/service/BookingService.kt | 6 +- .../workspace/routes/WorkspaceRoutingV1.kt | 3 +- 3 files changed, 86 insertions(+), 53 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt index 17858e489..1ba7d0959 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt @@ -349,7 +349,7 @@ class BookingMeetingRepository( val event = googleCalendarConverter.toGoogleEvent(booking) val savedEvent = calendar.Events().insert(defaultCalendar, event).execute() - if (checkBookingAvailable(savedEvent, workspaceCalendar)) { + if (recurringEventHasCollision(savedEvent, workspaceCalendar)) { val savedBooking = googleCalendarConverter.toBookingModel(savedEvent) logger.trace("[save] saved booking: {}", savedBooking) return savedBooking @@ -376,65 +376,65 @@ class BookingMeetingRepository( val workspaceCalendar = calendarIdsRepository.findByWorkspace( booking.workspace.id ?: throw MissingIdException("Missing workspace id") ) - val bookingId = booking.id ?: throw MissingIdException("Update model must have id") - val previousVersionOfEvent = findByCalendarIdAndBookingId(bookingId) ?: throw InstanceNotFoundException( - WorkspaceBookingEntity::class, "Booking with id $bookingId not wound" - ) - logger.trace("[update] previous version of event: {}", previousVersionOfEvent) - val eventOnUpdate = googleCalendarConverter.toGoogleEvent(booking) - - val updatedEvent: Event = calendarEvents.update(defaultCalendar, bookingId, eventOnUpdate).execute() - - val sequence = updatedEvent.sequence - if (checkBookingAvailable(updatedEvent, workspaceCalendar)) { - val updatedBooking = googleCalendarConverter.toBookingModel(updatedEvent) - logger.trace("[update] updated booking: {}", updatedBooking) - return updatedBooking + val updatedEvent: Event = if (booking.recurrence != null) { + updateRecurringEvent(booking, workspaceCalendar) } else { - previousVersionOfEvent.sequence = sequence - calendarEvents.update(defaultCalendar, bookingId, previousVersionOfEvent).execute() + updateSingleEvent(booking, workspaceCalendar) + } + return googleCalendarConverter.toBookingModel(updatedEvent) + .also { updatedBooking -> + logger.trace("[update] updated booking: {}", updatedBooking) + } + } + + /** + * Saving booking without recurrence. Checks collision before updating an event. + */ + private fun updateSingleEvent(booking: Booking, workspaceCalendar: String): Event { + val eventOnUpdate = googleCalendarConverter.toGoogleEvent(booking) + if (singleEventHasCollision(eventOnUpdate, workspaceCalendar)) { throw WorkspaceUnavailableException("Workspace ${booking.workspace.name} " + "unavailable at time between ${booking.beginBooking} and ${booking.endBooking}") } + + val eventId = eventOnUpdate.id + val prevEventVersion = findByCalendarIdAndBookingId(eventId) ?: throw InstanceNotFoundException( + WorkspaceBookingEntity::class, "Booking with id $eventId not wound" + ) + logger.trace("[updateSingleEvent] previous version of event: {}", prevEventVersion) + + return calendarEvents.update(defaultCalendar, eventId, eventOnUpdate).execute() } /** - * Launch checkSingleEventCollision for non-cycle event - * or receive instances for recurrent event and checks all instances. + * Saving booking with recurrence. Checks collision for all event instances after its update. * - * @param incomingEvent: [Event] - Must take only SAVED event - * @return Boolean. True if booking available - * */ - private fun checkBookingAvailable(incomingEvent: Event, workspaceCalendar: String): Boolean { - logger.debug( - "[checkBookingAvailable] checking if workspace with calendar id={} available for event {}", - workspaceCalendar, - incomingEvent - )//TODO: нам не обязательно сохранять ивент перед коллизией, если он не циклический - - var result = true - if (incomingEvent.recurrence != null) { - //TODO: Check, if we can receive instances without pushing this event into calendar - val instances = calendarEvents.instances(workspaceCalendar, incomingEvent.id) - .setMaxResults(50) - .execute().items - - for (instance in instances) { - if (singleEventHasCollision(instance, workspaceCalendar)) { - result = false - } - } - } else { - result = !singleEventHasCollision(incomingEvent, workspaceCalendar) + * @param booking updated booking to be saved + * @param workspaceCalendar calendar id for saving + */ + private fun updateRecurringEvent(booking: Booking, workspaceCalendar: String): Event { + val bookingId = booking.id ?: throw MissingIdException("Update model must have id") + val prevEventVersion = findByCalendarIdAndBookingId(bookingId) ?: throw InstanceNotFoundException( + WorkspaceBookingEntity::class, "Booking with id $bookingId not wound" + ) + logger.trace("[updateRecurringEvent] previous version of event: {}", prevEventVersion) + val eventOnUpdate = googleCalendarConverter.toGoogleEvent(booking) + + val updatedEvent: Event = calendarEvents.update(defaultCalendar, bookingId, eventOnUpdate).execute() + + val sequence = updatedEvent.sequence + if (recurringEventHasCollision(updatedEvent, workspaceCalendar)) { + prevEventVersion.sequence = sequence + calendarEvents.update(defaultCalendar, bookingId, prevEventVersion).execute() + throw WorkspaceUnavailableException("Workspace ${booking.workspace.name} unavailable at specified time.") } - logger.debug("[checkBookingAvailable] result {}", result) - return result + return updatedEvent } /** * Checks weather the saved event has collision with other events. * - * @param eventToVerify: [Event] - event for collision check. Must be saved before check + * @param eventToVerify event for collision check * */ private fun singleEventHasCollision(eventToVerify: Event, workspaceCalendar: String): Boolean { val sameTimeEvents = basicQuery( @@ -452,14 +452,50 @@ class BookingMeetingRepository( return false } + /** + * Launch checkSingleEventCollision for non-cycle event + * or receive instances for recurrent event and checks all instances. + * + * @param incomingEvent: [Event] - Must take only SAVED event + * @return Boolean. True if booking available + * */ + private fun recurringEventHasCollision(incomingEvent: Event, workspaceCalendar: String): Boolean { + logger.debug( + "[checkBookingAvailable] checking if workspace with calendar id={} available for event {}", + workspaceCalendar, + incomingEvent + ) + + var result = false + //TODO: Check, if we can receive instances without pushing this event into calendar + val instances = calendarEvents.instances(workspaceCalendar, incomingEvent.id) + .setMaxResults(50) + .execute().items + + for (instance in instances) { + if (singleEventHasCollision(instance, workspaceCalendar)) { + result = true + } + } + logger.debug("[recurringEventHasCollision] result: {}", result) + return result + } + + /** + * Checks whether events has collision + */ private fun areEventsOverlap(firstEvent: Event, secondEvent: Event): Boolean { return secondEvent.start.dateTime.value < firstEvent.end.dateTime.value && secondEvent.end.dateTime.value > firstEvent.start.dateTime.value } + /** + * Checks whether events aren't the same event or instances of the same event + */ private fun eventsIsNotSame(firstEvent: Event, secondEvent: Event): Boolean { return firstEvent.id != secondEvent.id && firstEvent.id != secondEvent.recurringEventId && - firstEvent.recurringEventId != secondEvent.id + firstEvent.recurringEventId != secondEvent.id && + (firstEvent.recurringEventId != firstEvent.recurringEventId || firstEvent.recurringEventId == null) } } \ No newline at end of file diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt index d0082bc61..5b1c043d3 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt @@ -282,14 +282,10 @@ class BookingService( * * @param booking changed booking * @return [Booking] after change saving - * @throws InstanceNotFoundException if workspace with the given id not found * @author Daniil Zavyalov */ override fun update(booking: Booking): Booking { - val workspaceId = booking.workspace.id ?: throw MissingIdException("Missing booking workspace id") - val workspace = workspaceRepository.findById(workspaceId) - ?: throw InstanceNotFoundException(WorkspaceBookingEntity::class, "Workspace with id $workspaceId not wound") - return if (workspace.tag == "meeting") { + return if (booking.workspace.tag == "meeting") { logger.error("Updating meeting room booking") bookingMeetingRepository.update(booking) } else { diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt index 5b0228774..dc8894f18 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt @@ -18,6 +18,7 @@ import java.time.ZoneId fun Route.workspaceRoutingV1() { route("/api/v1/workspaces") { val facade: WorkspaceFacadeV1 = GlobalContext.get().get() + val oneDayMillis: Long = 1000*60*60*24; get("/{id}", SwaggerDocument.returnWorkspaceByIdV1()) { val id: String = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest) @@ -44,7 +45,7 @@ fun Route.workspaceRoutingV1() { .atStartOfDay(ZoneId.of(BookingConstants.DEFAULT_TIMEZONE_ID)) .toInstant() .toEpochMilli() - val endOfDayEpoch = todayEpoch + 1000*60*60*24 + val endOfDayEpoch = todayEpoch + oneDayMillis withBookingsFrom = withBookingsFromString?.let { paramString -> paramString.toLongOrNull() ?: throw BadRequestException("with_bookings_from can't be parsed to Long") From b959832146acb0be4ec7b1dddaa4f1e7648f18e4 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Fri, 19 Apr 2024 22:46:10 +0600 Subject: [PATCH 12/26] [+] add returnInstances param for get bookings --- .../features/booking/facade/BookingFacade.kt | 1 + .../booking/facade/BookingFacadeV1.kt | 5 +- .../repository/BookingMeetingRepository.kt | 43 +++++++++---- .../repository/BookingRegularRepository.kt | 31 +++++++--- .../booking/repository/IBookingRepository.kt | 20 ++++-- .../booking/routes/BookingRoutingV1.kt | 7 ++- .../routes/swagger/BookingSwaggerV1.kt | 12 ++-- .../booking/service/BookingService.kt | 62 ++++++++++++------- .../workspace/facade/WorkspaceFacadeV1.kt | 1 + .../workspace/service/WorkspaceService.kt | 1 - .../effective/serviceapi/IBookingService.kt | 2 + 11 files changed, 129 insertions(+), 56 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt index 0bdbc5b06..742fed5da 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacade.kt @@ -85,6 +85,7 @@ class BookingFacade( bookingService.findAll( userId?.let { uuidValidator.uuidFromString(it) }, workspaceId?.let { uuidValidator.uuidFromString(it) }, + true, bookingRangeTo, bookingRangeFrom ) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt index a9e2e2cb8..a706bf5e7 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt @@ -64,6 +64,7 @@ class BookingFacadeV1( * Should be greater than range_from. * @param bookingRangeFrom lower bound (exclusive) for a endBooking to filter by. * Should be lover than [bookingRangeFrom]. Default value: [BookingConstants.MIN_SEARCH_START_TIME] + * @param returnInstances return recurring bookings as non-recurrent instances * @return [BookingDTO] list * @author Daniil Zavyalov */ @@ -71,7 +72,8 @@ class BookingFacadeV1( userId: String?, workspaceId: String?, bookingRangeTo: Long?, - bookingRangeFrom: Long = BookingConstants.MIN_SEARCH_START_TIME + bookingRangeFrom: Long = BookingConstants.MIN_SEARCH_START_TIME, + returnInstances: Boolean ): List { if (bookingRangeTo != null && bookingRangeTo <= bookingRangeFrom) { throw BadRequestException("Max booking start time should be null or greater than min start time") @@ -80,6 +82,7 @@ class BookingFacadeV1( bookingService.findAll( userId?.let { uuidValidator.uuidFromString(it) }, workspaceId?.let { uuidValidator.uuidFromString(it) }, + returnInstances, bookingRangeTo, bookingRangeFrom ) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt index 1ba7d0959..89bec4c84 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt @@ -98,7 +98,10 @@ class BookingMeetingRepository( * @return [Event] with the given [bookingId] from calendar with [calendarId] * or null if event with the given id doesn't exist */ - private fun findByCalendarIdAndBookingId(bookingId: String, calendarId: String = defaultCalendar): Event? { + private fun findByCalendarIdAndBookingId( + bookingId: String, + calendarId: String = defaultCalendar + ): Event? { logger.trace("Retrieving event from {} calendar by id", calendarId) return try { calendar.events().get(calendarId, bookingId).execute() @@ -133,12 +136,18 @@ class BookingMeetingRepository( * Returns all bookings with the given workspace id * * @param workspaceId + * @param returnInstances return recurring bookings as non-recurrent instances * @param eventRangeFrom lower bound (exclusive) for a endBooking to filter by. * Old Google calendar events may not appear correctly in the system and cause unexpected exceptions * @param eventRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. * @return List of all workspace [Booking] */ - override fun findAllByWorkspaceId(workspaceId: UUID, eventRangeFrom: Long, eventRangeTo: Long?): List { + override fun findAllByWorkspaceId( + workspaceId: UUID, + eventRangeFrom: Long, + eventRangeTo: Long?, + returnInstances: Boolean + ): List { logger.debug( "[findAllByWorkspaceId] retrieving all bookings for workspace with id={} in range from {} to {}", workspaceId, @@ -146,8 +155,7 @@ class BookingMeetingRepository( eventRangeTo?.let { Instant.ofEpochMilli(it) } ?: "infinity" ) val workspaceCalendarId = getCalendarIdByWorkspace(workspaceId) - val getSingleEvents = true - val eventsWithWorkspace = basicQuery(eventRangeFrom, eventRangeTo, getSingleEvents, workspaceCalendarId) + val eventsWithWorkspace = basicQuery(eventRangeFrom, eventRangeTo, returnInstances, workspaceCalendarId) .execute().items return eventsWithWorkspace.toList().map { googleCalendarConverter.toBookingModel(it) } @@ -187,6 +195,7 @@ class BookingMeetingRepository( private fun getEventsWithQParam( calendarIds: List, q: String, + singleEvents: Boolean, eventRangeFrom: Long, eventRangeTo: Long? ): List { @@ -196,7 +205,7 @@ class BookingMeetingRepository( basicQuery( timeMin = eventRangeFrom, timeMax = eventRangeTo, - singleEvents = true, + singleEvents = singleEvents, calendarId = calendarId ).setQ(q) .execute().items @@ -214,6 +223,7 @@ class BookingMeetingRepository( private fun getAllEvents( calendarIds: List, + singleEvents: Boolean, eventRangeFrom: Long, eventRangeTo: Long? ): List { @@ -223,7 +233,7 @@ class BookingMeetingRepository( basicQuery( timeMin = eventRangeFrom, timeMax = eventRangeTo, - singleEvents = true, + singleEvents = singleEvents, calendarId = calendarId ).execute().items } @@ -245,11 +255,17 @@ class BookingMeetingRepository( * @param eventRangeFrom lower bound (exclusive) for a endBooking to filter by. * Old Google calendar events may not appear correctly in the system and cause unexpected exceptions * @param eventRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. + * @param returnInstances return recurring bookings as non-recurrent instances * @return List of all user [Booking] * @throws InstanceNotFoundException if user with the given id doesn't exist in database * @author Daniil Zavyalov, Danil Kiselev */ - override fun findAllByOwnerId(ownerId: UUID, eventRangeFrom: Long, eventRangeTo: Long?): List { + override fun findAllByOwnerId( + ownerId: UUID, + eventRangeFrom: Long, + eventRangeTo: Long?, + returnInstances: Boolean + ): List { logger.debug( "[findAllByOwnerId] retrieving all bookings for user with id={} in range from {} to {}", ownerId, @@ -259,7 +275,7 @@ class BookingMeetingRepository( val userEmail: String = findUserEmailByUserId(ownerId) val calendars: List = calendarIdsRepository.findAllCalendarsId() - val eventsWithUser = getEventsWithQParam(calendars, userEmail, eventRangeFrom, eventRangeTo) + val eventsWithUser = getEventsWithQParam(calendars, userEmail, returnInstances, eventRangeFrom, eventRangeTo) val result = mutableListOf() for (event in eventsWithUser) { @@ -280,13 +296,15 @@ class BookingMeetingRepository( * @param eventRangeFrom lower bound (exclusive) for a endBooking to filter by. * Old Google calendar events may not appear correctly in the system and cause unexpected exceptions * @param eventRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. + * @param returnInstances return recurring bookings as non-recurrent instances * @return List of all [Booking]s with the given workspace and owner id */ override fun findAllByOwnerAndWorkspaceId( ownerId: UUID, workspaceId: UUID, eventRangeFrom: Long, - eventRangeTo: Long? + eventRangeTo: Long?, + returnInstances: Boolean ): List { logger.debug( "[findAllByOwnerAndWorkspaceId] retrieving all bookings for a workspace with id={} created by user with id={} in range from {} to {}", @@ -298,7 +316,7 @@ class BookingMeetingRepository( val userEmail: String = findUserEmailByUserId(ownerId) val workspaceCalendarId = getCalendarIdByWorkspace(workspaceId) - val eventsWithUserAndWorkspace = basicQuery(eventRangeFrom, eventRangeTo, true, workspaceCalendarId) + val eventsWithUserAndWorkspace = basicQuery(eventRangeFrom, eventRangeTo, returnInstances, workspaceCalendarId) .setQ(userEmail) .execute().items @@ -320,16 +338,17 @@ class BookingMeetingRepository( * @param eventRangeFrom lower bound (exclusive) for a endBooking to filter by. * Old Google calendar events may not appear correctly in the system and cause unexpected exceptions * @param eventRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. + * @param returnInstances return recurring bookings as non-recurrent instances * @return All [Booking]s */ - override fun findAll(eventRangeFrom: Long, eventRangeTo: Long?): List { + override fun findAll(eventRangeFrom: Long, eventRangeTo: Long?, returnInstances: Boolean): List { logger.debug( "[findAll] retrieving all bookings in range from {} to {}", Instant.ofEpochMilli(eventRangeFrom), eventRangeTo?.let { Instant.ofEpochMilli(it) } ?: "infinity" ) val calendars: List = calendarIdsRepository.findAllCalendarsId() - val events: List = getAllEvents(calendars, eventRangeFrom, eventRangeTo) + val events: List = getAllEvents(calendars, returnInstances, eventRangeFrom, eventRangeTo) return events.map { googleCalendarConverter.toBookingModel(it) } } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt index 00cc2a838..4e61ec9c5 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt @@ -125,17 +125,23 @@ class BookingRegularRepository( * @param eventRangeFrom lower bound (exclusive) for a endBooking to filter by. * Old Google calendar events may not appear correctly in the system and cause unexpected exceptions * @param eventRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. + * @param returnInstances return recurring bookings as non-recurrent instances * @return List of all workspace [Booking] * @author Daniil Zavyalov */ - override fun findAllByWorkspaceId(workspaceId: UUID, eventRangeFrom: Long, eventRangeTo: Long?): List { + override fun findAllByWorkspaceId( + workspaceId: UUID, + eventRangeFrom: Long, + eventRangeTo: Long?, + returnInstances: Boolean + ): List { logger.debug( "[findAllByWorkspaceId] retrieving all bookings for workspace with id={} in range from {} to {}", workspaceId, Instant.ofEpochMilli(eventRangeFrom), eventRangeTo?.let { Instant.ofEpochMilli(it) } ?: "infinity" ) - val eventsWithWorkspace = basicQuery(eventRangeFrom, eventRangeTo) + val eventsWithWorkspace = basicQuery(eventRangeFrom, eventRangeTo, returnInstances) .setQ(workspaceId.toString()) .execute().items logger.trace("[findAllByWorkspaceId] request to Google Calendar completed") @@ -153,18 +159,24 @@ class BookingRegularRepository( * @param eventRangeFrom lower bound (exclusive) for a endBooking to filter by. * Old Google calendar events may not appear correctly in the system and cause unexpected exceptions * @param eventRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. + * @param returnInstances return recurring bookings as non-recurrent instances * @return List of all user [Booking] * @throws InstanceNotFoundException if user with the given id doesn't exist in database * @author Daniil Zavyalov */ - override fun findAllByOwnerId(ownerId: UUID, eventRangeFrom: Long, eventRangeTo: Long?): List { + override fun findAllByOwnerId( + ownerId: UUID, + eventRangeFrom: Long, + eventRangeTo: Long?, + returnInstances: Boolean + ): List { logger.debug( "[findAllByOwnerId] retrieving all bookings for user with id={} in range from {} to {}", ownerId, Instant.ofEpochMilli(eventRangeFrom), eventRangeTo?.let { Instant.ofEpochMilli(it) } ?: "infinity" ) - val eventsWithUser = basicQuery(eventRangeFrom, eventRangeTo) + val eventsWithUser = basicQuery(eventRangeFrom, eventRangeTo, returnInstances) .setQ(ownerId.toString()) .execute().items logger.trace("[findAllByOwnerId] request to Google Calendar completed") @@ -182,6 +194,7 @@ class BookingRegularRepository( * @param eventRangeFrom lower bound (exclusive) for a endBooking to filter by. * Old Google calendar events may not appear correctly in the system and cause unexpected exceptions * @param eventRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. + * @param returnInstances return recurring bookings as non-recurrent instances * @return List of all [Booking]s with the given workspace and owner id * @author Daniil Zavyalov */ @@ -189,7 +202,8 @@ class BookingRegularRepository( ownerId: UUID, workspaceId: UUID, eventRangeFrom: Long, - eventRangeTo: Long? + eventRangeTo: Long?, + returnInstances: Boolean ): List { logger.debug( "[findAllByOwnerAndWorkspaceId] retrieving all bookings for a workspace with id={} created by user with id={} in range from {} to {}", @@ -198,7 +212,7 @@ class BookingRegularRepository( Instant.ofEpochMilli(eventRangeFrom), eventRangeTo?.let { Instant.ofEpochMilli(it) } ?: "infinity" ) - val eventsWithUserAndWorkspace = basicQuery(eventRangeFrom, eventRangeTo) + val eventsWithUserAndWorkspace = basicQuery(eventRangeFrom, eventRangeTo, returnInstances) .setQ("$workspaceId $ownerId") .execute().items logger.trace("[findAllByOwnerAndWorkspaceId] request to Google Calendar completed") @@ -214,16 +228,17 @@ class BookingRegularRepository( * @param eventRangeFrom lower bound (exclusive) for a endBooking to filter by. * Old Google calendar events may not appear correctly in the system and cause unexpected exceptions * @param eventRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. + * @param returnInstances return recurring bookings as non-recurrent instances * @return All [Booking]s * @author Daniil Zavyalov */ - override fun findAll(eventRangeFrom: Long, eventRangeTo: Long?): List { + override fun findAll(eventRangeFrom: Long, eventRangeTo: Long?, returnInstances: Boolean): List { logger.debug( "[findAll] retrieving all bookings in range from {} to {}", Instant.ofEpochMilli(eventRangeFrom), eventRangeTo?.let { Instant.ofEpochMilli(it) } ?: "infinity" ) - val events = basicQuery(eventRangeFrom, eventRangeTo).execute().items + val events = basicQuery(eventRangeFrom, eventRangeTo, returnInstances).execute().items logger.trace("[findAll] request to Google Calendar completed") return events.map { event -> diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/IBookingRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/IBookingRepository.kt index 50573932d..ef25c390a 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/IBookingRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/IBookingRepository.kt @@ -41,10 +41,17 @@ interface IBookingRepository { * @param ownerId * @param eventRangeTo use to set an upper bound for filtering bookings by start time * @param eventRangeFrom lover bound for filtering bookings by start time + * @param returnInstances return recurring bookings as non-recurrent instances + * * @return List of all user [Booking] * @author Daniil Zavyalov, Danil Kiselev */ - fun findAllByOwnerId(ownerId: UUID, eventRangeFrom: Long, eventRangeTo: Long? = null): List + fun findAllByOwnerId( + ownerId: UUID, + eventRangeFrom: Long, + eventRangeTo: Long? = null, + returnInstances: Boolean = true, + ): List /** * Returns all bookings with the given workspace id @@ -52,13 +59,15 @@ interface IBookingRepository { * @param workspaceId * @param eventRangeFrom use to set an upper bound for filtering bookings by start time * @param eventRangeTo lover bound for filtering bookings by start time + * @param returnInstances return recurring bookings as non-recurrent instances * @return List of all workspace [Booking] * @author Daniil Zavyalov, Danil Kiselev */ fun findAllByWorkspaceId( workspaceId: UUID, eventRangeFrom: Long, - eventRangeTo: Long? = null + eventRangeTo: Long? = null, + returnInstances: Boolean = true, ): List /** @@ -68,6 +77,7 @@ interface IBookingRepository { * @param workspaceId * @param eventRangeFrom use to set an upper bound for filtering bookings by start time * @param eventRangeTo lover bound for filtering bookings by start time + * @param returnInstances return recurring bookings as non-recurrent instances * @return List of all [Booking]s with the given workspace and owner id * @author Daniil Zavyalov, Danil Kiselev */ @@ -75,7 +85,8 @@ interface IBookingRepository { ownerId: UUID, workspaceId: UUID, eventRangeFrom: Long, - eventRangeTo: Long? = null + eventRangeTo: Long? = null, + returnInstances: Boolean = true, ): List /** @@ -83,10 +94,11 @@ interface IBookingRepository { * * @param eventRangeFrom use to set an upper bound for filtering bookings by start time * @param eventRangeTo lover bound for filtering bookings by start time + * @param returnInstances return recurring bookings as non-recurrent instances * @return All [Booking]s * @author Daniil Zavyalov, Danil Kiselev */ - fun findAll(eventRangeFrom: Long, eventRangeTo: Long? = null): List + fun findAll(eventRangeFrom: Long, eventRangeTo: Long? = null, returnInstances: Boolean = true): List /** * Saves a given booking. If given model will have an id, it will be ignored. diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt index 2e769f29d..bb3bd915b 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt @@ -38,6 +38,11 @@ fun Route.bookingRoutingV1() { val userId: String? = call.request.queryParameters["user_id"] val workspaceId: String? = call.request.queryParameters["workspace_id"] + + val returnInstances: Boolean = call.request.queryParameters["return_instances"]?.let { stringRangeTo -> + stringRangeTo.toBooleanStrictOrNull() + ?: throw BadRequestException("return_instances can't be parsed to Boolean") + } ?: true val bookingRangeTo: Long = call.request.queryParameters["range_to"]?.let { stringRangeTo -> stringRangeTo.toLongOrNull() ?: throw BadRequestException("range_to can't be parsed to Long") @@ -47,7 +52,7 @@ fun Route.bookingRoutingV1() { ?: throw BadRequestException("range_from can't be parsed to Long") } ?: endOfDayEpoch - call.respond(bookingFacade.findAll(userId, workspaceId, bookingRangeTo, bookingRangeFrom)) + call.respond(bookingFacade.findAll(userId, workspaceId, bookingRangeTo, bookingRangeFrom, returnInstances)) } post(SwaggerDocument.postBookingV1()) { val dto = call.receive() diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt index 8b3323890..16d31834f 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt @@ -19,11 +19,11 @@ import java.time.Instant */ fun SwaggerDocument.returnBookingByIdV1(): OpenApiRoute.() -> Unit = { description = "Returns booking found by id" - tags = listOf("bookings") + tags = listOf("Bookings V1") request { pathParameter("id") { description = "Booking id" - example = "c48c2a3d-bbfd-4801-b121-973ae3cf4cd9" + example = "p0v9udrhk66cailnigi0qkrji4" required = true allowEmptyValue = false } @@ -53,7 +53,7 @@ fun SwaggerDocument.returnBookingsV1(): OpenApiRoute.() -> Unit = { description = "Return all bookings. Bookings can be filtered by booking owner id, workspace id and time range. " + "Returns only non-recurring bookings (recurring bookings are expanded into non-recurring ones). " + "Can return no more than 2500 bookings." - tags = listOf("bookings") + tags = listOf("Bookings V1") request { queryParameter("user_id") { description = "Booking owner id" @@ -108,7 +108,7 @@ fun SwaggerDocument.returnBookingsV1(): OpenApiRoute.() -> Unit = { */ fun SwaggerDocument.postBookingV1(): OpenApiRoute.() -> Unit = { description = "Saves a given booking" - tags = listOf("bookings") + tags = listOf("Bookings V1") request { body { example( @@ -194,7 +194,7 @@ fun SwaggerDocument.postBookingV1(): OpenApiRoute.() -> Unit = { */ fun SwaggerDocument.putBookingV1(): OpenApiRoute.() -> Unit = { description = "Updates a given booking" - tags = listOf("bookings") + tags = listOf("Bookings V1") request { body { example( @@ -232,7 +232,7 @@ fun SwaggerDocument.putBookingV1(): OpenApiRoute.() -> Unit = { fun SwaggerDocument.deleteBookingByIdV1(): OpenApiRoute.() -> Unit = { description = "Deletes a booking with the given id. If the booking is not found in the database it is silently ignored" - tags = listOf("bookings") + tags = listOf("Bookings V1") request { pathParameter("id") { description = "Booking id" diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt index 5b1c043d3..ad01e95c5 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/service/BookingService.kt @@ -83,6 +83,7 @@ class BookingService( * * @param userId use to filter by booking owner id * @param workspaceId use to filter by booking workspace id + * @param returnInstances return recurring bookings as non-recurrent instances * @param bookingRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. * @param bookingRangeFrom lower bound (exclusive) for a endBooking to filter by. * @throws InstanceNotFoundException if [UserModel] or [Workspace] with the given id doesn't exist in database @@ -91,6 +92,7 @@ class BookingService( override fun findAll( userId: UUID?, workspaceId: UUID?, + returnInstances: Boolean, bookingRangeTo: Long?, bookingRangeFrom: Long ): List { @@ -106,17 +108,19 @@ class BookingService( if (workspace.tag == "meeting") { bookingMeetingRepository.findAllByOwnerAndWorkspaceId( - userId, - workspaceId, - bookingRangeFrom, - bookingRangeTo + ownerId = userId, + workspaceId = workspaceId, + returnInstances = returnInstances, + eventRangeFrom = bookingRangeFrom, + eventRangeTo = bookingRangeTo ) } else { bookingRegularRepository.findAllByOwnerAndWorkspaceId( - userId, - workspaceId, - bookingRangeFrom, - bookingRangeTo + ownerId = userId, + workspaceId = workspaceId, + returnInstances = returnInstances, + eventRangeFrom = bookingRangeFrom, + eventRangeTo = bookingRangeTo ) } } @@ -127,15 +131,17 @@ class BookingService( ) val bookings = bookingMeetingRepository.findAllByOwnerId( - userId, - bookingRangeFrom, - bookingRangeTo - ).toMutableList() + ownerId = userId, + returnInstances = returnInstances, + eventRangeFrom = bookingRangeFrom, + eventRangeTo = bookingRangeTo + ).toMutableList() bookings.addAll( bookingRegularRepository.findAllByOwnerId( - userId, - bookingRangeFrom, - bookingRangeTo + ownerId = userId, + returnInstances = returnInstances, + eventRangeFrom = bookingRangeFrom, + eventRangeTo = bookingRangeTo ) ) bookings @@ -149,23 +155,33 @@ class BookingService( if (workspace.tag == "meeting") { bookingMeetingRepository.findAllByWorkspaceId( - workspaceId, - bookingRangeFrom, - bookingRangeTo + workspaceId = workspaceId, + returnInstances = returnInstances, + eventRangeFrom = bookingRangeFrom, + eventRangeTo = bookingRangeTo ) } else { bookingRegularRepository.findAllByWorkspaceId( - workspaceId, - bookingRangeFrom, - bookingRangeTo + workspaceId = workspaceId, + returnInstances = returnInstances, + eventRangeFrom = bookingRangeFrom, + eventRangeTo = bookingRangeTo ) } } else -> { - val bookings = bookingMeetingRepository.findAll(bookingRangeFrom, bookingRangeTo).toMutableList() - bookings.addAll(bookingRegularRepository.findAll(bookingRangeFrom, bookingRangeTo)) + val bookings = bookingMeetingRepository.findAll( + eventRangeFrom = bookingRangeFrom, + eventRangeTo = bookingRangeTo, + returnInstances = returnInstances + ).toMutableList() + bookings.addAll(bookingRegularRepository.findAll( + eventRangeFrom = bookingRangeFrom, + eventRangeTo = bookingRangeTo, + returnInstances = returnInstances + )) bookings } } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt index 098abcf38..089111343 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt @@ -73,6 +73,7 @@ class WorkspaceFacadeV1( val bookings = bookingFacade.findAll( userId = null, workspaceId = workspace.id.toString(), + returnInstances = true, bookingRangeFrom = withBookingsFrom, bookingRangeTo = withBookingsUntil ) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/service/WorkspaceService.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/service/WorkspaceService.kt index 424cc78ee..c97896865 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/service/WorkspaceService.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/service/WorkspaceService.kt @@ -1,6 +1,5 @@ package office.effective.features.workspace.service -import office.effective.features.booking.service.BookingService import office.effective.features.workspace.repository.WorkspaceRepository import office.effective.model.Workspace import office.effective.model.WorkspaceZone diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/serviceapi/IBookingService.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/serviceapi/IBookingService.kt index f07f78f33..b6e367dc4 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/serviceapi/IBookingService.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/serviceapi/IBookingService.kt @@ -40,6 +40,7 @@ interface IBookingService { * * @param userId use to filter by booking owner id * @param workspaceId use to filter by booking workspace id + * @param returnInstances return recurring bookings as non-recurrent instances * @param bookingRangeTo upper bound (exclusive) for a beginBooking to filter by. Optional. * @param bookingRangeFrom lower bound (exclusive) for a endBooking to filter by. * @throws InstanceNotFoundException if [UserModel] or [Workspace] with the given id doesn't exist in database @@ -48,6 +49,7 @@ interface IBookingService { fun findAll( userId: UUID? = null, workspaceId: UUID? = null, + returnInstances: Boolean = true, bookingRangeTo: Long? = null, bookingRangeFrom: Long ): List From 1957204d03af27ad6414ea433796613e3620f546 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Fri, 19 Apr 2024 23:38:03 +0600 Subject: [PATCH 13/26] [~] fix swagger --- .../routes/swagger/BookingSwaggerV1.kt | 160 ++++++++---------- .../routes/CalendarNotificationsRouting.kt | 7 +- .../swagger/CalendarNotificationsSwagger.kt | 23 +++ .../user/routes/swagger/UserSwaggerV1.kt | 6 +- .../routes/swagger/WorkspaceSwaggerV1.kt | 30 ++-- 5 files changed, 113 insertions(+), 113 deletions(-) create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/features/notifications/routes/swagger/CalendarNotificationsSwagger.kt diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt index 16d31834f..4c02a060b 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt @@ -7,11 +7,7 @@ import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.ktor.http.* import office.effective.common.constants.BookingConstants import office.effective.common.swagger.SwaggerDocument -import office.effective.dto.BookingDTO -import office.effective.dto.IntegrationDTO -import office.effective.dto.UserDTO -import office.effective.dto.UtilityDTO -import office.effective.dto.WorkspaceDTO +import office.effective.dto.* import java.time.Instant /** @@ -31,9 +27,9 @@ fun SwaggerDocument.returnBookingByIdV1(): OpenApiRoute.() -> Unit = { response { HttpStatusCode.OK to { description = "Returns booking found by id" - body { + body { example( - "Bookings", bookingExample1 + "Bookings", bookingResponseExample1 ) {} } } @@ -51,29 +47,37 @@ fun SwaggerDocument.returnBookingByIdV1(): OpenApiRoute.() -> Unit = { */ fun SwaggerDocument.returnBookingsV1(): OpenApiRoute.() -> Unit = { description = "Return all bookings. Bookings can be filtered by booking owner id, workspace id and time range. " + - "Returns only non-recurring bookings (recurring bookings are expanded into non-recurring ones). " + + "By default returns only non-recurring bookings (recurring bookings are expanded into non-recurring ones). " + "Can return no more than 2500 bookings." tags = listOf("Bookings V1") request { queryParameter("user_id") { - description = "Booking owner id" + description = "Booking owner UUID" example = "2c77feee-2bc1-11ee-be56-0242ac120002" required = false allowEmptyValue = false } queryParameter("workspace_id") { - description = "Booked workspace id" + description = "Booked workspace UUID" example = "50d89406-2bc6-11ee-be56-0242ac120002" required = false allowEmptyValue = false } + queryParameter("return_instances") { + description = "Whether to expand recurring bookings into instances " + + "and only return single one-off bookings and instances of recurring bookings, but not the " + + "underlying recurring bookings themselves." + + "Default value is true" + example = true + required = false + allowEmptyValue = false + } queryParameter("range_from") { description = "Lower bound (exclusive) for a endBooking to filter by. Should be lover than range_to. " + "The default value is the beginning of today." example = 1692927200000 required = false allowEmptyValue = false - } queryParameter("range_to") { description = "Upper bound (exclusive) for a beginBooking to filter by. " + @@ -86,16 +90,16 @@ fun SwaggerDocument.returnBookingsV1(): OpenApiRoute.() -> Unit = { response { HttpStatusCode.OK to { description = "Returns all bookings found by user id" - body> { + body> { example( "Workspace", listOf( - bookingExample1, bookingExample2 + bookingResponseExample1, bookingResponseExample2 ) ) {} } } HttpStatusCode.BadRequest to { - description = "range_to isn't greater then range_to, or one of them can't be parsed to Long" + description = "range_to isn't greater then range_to, or one of the parameters has an incorrect type" } HttpStatusCode.NotFound to { description = "User or workspace with the given id doesn't exist" @@ -110,73 +114,16 @@ fun SwaggerDocument.postBookingV1(): OpenApiRoute.() -> Unit = { description = "Saves a given booking" tags = listOf("Bookings V1") request { - body { - example( - "Bookings", BookingDTO( - owner = UserDTO( - id = "2c77feee-2bc1-11ee-be56-0242ac120002", - fullName = "Max", - active = true, - role = "Fullstack developer", - avatarUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", - integrations = listOf( - IntegrationDTO( - "c717cf6e-28b3-4148-a469-032991e5d9e9", - "phoneNumber", - "89087659880" - ) - ), - email = "cool.fullstack.developer@effective.band", - tag = "employee" - ), - participants = listOf( - UserDTO( - id = "2c77feee-2bc1-11ee-be56-0242ac120002", - fullName = "Ivan Ivanov", - active = true, - role = "Android developer", - avatarUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", - integrations = listOf( - IntegrationDTO( - "c717cf6e-28b3-4148-a469-032991e5d9e9", - "phoneNumber", - "89236379887" - ) - ), - email = "cool.backend.developer@effective.band", - tag = "employee" - ) - ), - workspace = WorkspaceDTO( - id = "2561471e-2bc6-11ee-be56-0242ac120002", name = "Sun", tag = "meeting", - utilities = listOf( - UtilityDTO( - id = "50d89406-2bc6-11ee-be56-0242ac120002", - name = "Sockets", - iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", - count = 8 - ), UtilityDTO( - id = "a62a86c6-2bc6-11ee-be56-0242ac120002", - name = "Projectors", - iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", - count = 1 - ) - ) - ), - id = null, - beginBooking = 1691299526000, - endBooking = 1691310326000, - recurrence = null - ) - ) + body { + example("Bookings", bookingRequestExample1) } } response { HttpStatusCode.Created to { description = "Returns saved booking" - body { + body { example( - "Bookings", bookingExample2 + "Bookings", bookingResponseExample1 ) {} } } @@ -193,27 +140,30 @@ fun SwaggerDocument.postBookingV1(): OpenApiRoute.() -> Unit = { * @suppress */ fun SwaggerDocument.putBookingV1(): OpenApiRoute.() -> Unit = { - description = "Updates a given booking" + description = "Updates a given booking. " + + "Note that recurring bookings have different id's with their instances. " + + "If you have an instance and want to update a recurring booking " + + "you should request a recurring booking by recurringBookingId and then update the requested booking." tags = listOf("Bookings V1") request { - body { + body { example( - "Bookings", bookingExample1 + "Bookings", bookingRequestExample2 ) } pathParameter("id") { description = "Booking id" - example = "p0v9udrhk66cailnigi0qkrji4" + example = "x9v0udreksdcailnigi0qkras4" required = true allowEmptyValue = false } } response { HttpStatusCode.OK to { - description = "Returns saved booking" + description = "Returns a saved booking" body { example( - "Bookings", bookingExample1 + "Bookings", bookingResponseExample2 ) {} } } @@ -236,7 +186,7 @@ fun SwaggerDocument.deleteBookingByIdV1(): OpenApiRoute.() -> Unit = { request { pathParameter("id") { description = "Booking id" - example = "c48c2a3d-bbfd-4801-b121-973ae3cf4cd9" + example = "x9v0udreksdcailnigi0qkras4" required = true allowEmptyValue = false } @@ -254,7 +204,19 @@ fun SwaggerDocument.deleteBookingByIdV1(): OpenApiRoute.() -> Unit = { /** * @suppress */ -private val bookingExample1 = BookingDTO( +private val bookingRequestExample1 = BookingRequestDTO( + ownerEmail = "cool.backend.developer@effective.band", + participantEmails = listOf("cool.backend.developer@effective.band", "email@yahoo.com"), + workspaceId = "2561471e-2bc6-11ee-be56-0242ac120002", + beginBooking = 1691299526000, + endBooking = 1691310326000, + recurrence = null, +) + +/** + * @suppress + */ +private val bookingResponseExample1 = BookingResponseDTO( owner = UserDTO( id = "2c77feee-2bc1-11ee-be56-0242ac120002", fullName = "Ivan Ivanov", @@ -310,27 +272,40 @@ private val bookingExample1 = BookingDTO( utilities = listOf( UtilityDTO( id = "50d89406-2bc6-11ee-be56-0242ac120002", - name = "Sockets", + name = "Place", iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", count = 8 ), UtilityDTO( id = "a62a86c6-2bc6-11ee-be56-0242ac120002", - name = "Projectors", + name = "Sockets", iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", count = 1 ) ) ), - id = "c48c2a3d-bbfd-4801-b121-973ae3cf4cd9", + id = "p0v9udrhk66cailnigi0qkrji4", + beginBooking = 1691299526000, + endBooking = 1691310326000, + recurrence = null, + recurringBookingId = null +) + +/** + * @suppress + */ +private val bookingRequestExample2 = BookingRequestDTO( + ownerEmail = "cool.backend.developer@effective.band", + participantEmails = listOf("cool.backend.developer@effective.band"), + workspaceId = "2561471e-2bc6-11ee-be56-0242ac120002", beginBooking = 1691299526000, endBooking = 1691310326000, - recurrence = null + recurrence = null, ) /** * @suppress */ -private val bookingExample2 = BookingDTO( +private val bookingResponseExample2 = BookingResponseDTO( owner = UserDTO( id = "2c77feee-2bc1-11ee-be56-0242ac120002", fullName = "Ivan Ivanov", @@ -370,19 +345,20 @@ private val bookingExample2 = BookingDTO( utilities = listOf( UtilityDTO( id = "50d89406-2bc6-11ee-be56-0242ac120002", - name = "Sockets", + name = "Place", iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", count = 8 ), UtilityDTO( id = "a62a86c6-2bc6-11ee-be56-0242ac120002", - name = "Projectors", + name = "Sockets", iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", count = 1 ) ) ), - id = "c48c2a3d-bbfd-4801-b121-973ae3cf4cd9", + id = "x9v0udreksdcailnigi0qkras4", beginBooking = 1691299526000, endBooking = 1691310326000, - recurrence = null + recurrence = null, + recurringBookingId = null ) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/notifications/routes/CalendarNotificationsRouting.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/notifications/routes/CalendarNotificationsRouting.kt index 3480ee50e..849976912 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/notifications/routes/CalendarNotificationsRouting.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/notifications/routes/CalendarNotificationsRouting.kt @@ -1,5 +1,6 @@ package office.effective.features.notifications.routes +import io.github.smiley4.ktorswaggerui.dsl.post import io.github.smiley4.ktorswaggerui.dsl.route import io.ktor.http.* import io.ktor.server.application.* @@ -8,6 +9,8 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import office.effective.common.notifications.FcmNotificationSender import office.effective.common.notifications.INotificationSender +import office.effective.common.swagger.SwaggerDocument +import office.effective.features.notifications.routes.swagger.receiveNotification import org.koin.core.context.GlobalContext import org.slf4j.LoggerFactory @@ -18,9 +21,9 @@ fun Route.calendarNotificationsRouting() { route("/notifications") { val messageSender: INotificationSender = GlobalContext.get().get() - post() { + post(SwaggerDocument.receiveNotification()) { val logger = LoggerFactory.getLogger(FcmNotificationSender::class.java) - logger.info("[calendarNotificationsRouting] received push notification: \n{}", call.receive()) + logger.info("[calendarNotificationsRouting] received push notification: {}", call.receive()) messageSender.sendEmptyMessage("booking") call.respond(HttpStatusCode.OK) } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/notifications/routes/swagger/CalendarNotificationsSwagger.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/notifications/routes/swagger/CalendarNotificationsSwagger.kt new file mode 100644 index 000000000..0b1fe1f35 --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/notifications/routes/swagger/CalendarNotificationsSwagger.kt @@ -0,0 +1,23 @@ +/** + * @suppress + */ +package office.effective.features.notifications.routes.swagger + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.ktor.http.* +import office.effective.common.swagger.SwaggerDocument +import office.effective.dto.IntegrationDTO +import office.effective.dto.UserDTO + +/** + * @suppress + */ +fun SwaggerDocument.receiveNotification(): OpenApiRoute.() -> Unit = { + description = "Endpoint for receiving Google Calendar notifications" + tags = listOf("Notifications") + response { + HttpStatusCode.OK to { + description = "Notification was received" + } + } +} diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/UserSwaggerV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/UserSwaggerV1.kt index b4bfea827..78eeb7379 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/UserSwaggerV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/user/routes/swagger/UserSwaggerV1.kt @@ -14,7 +14,7 @@ import office.effective.dto.UserDTO */ fun SwaggerDocument.returnUsersV1(): OpenApiRoute.() -> Unit = { description = "Return all users, all users by tag or one user by email" - tags = listOf("users") + tags = listOf("Users V1") request{ queryParameter("user_tag"){ description = "Name of the tag. Mutually exclusive with email" @@ -119,7 +119,7 @@ fun SwaggerDocument.returnUsersV1(): OpenApiRoute.() -> Unit = { */ fun SwaggerDocument.returnUserByIdV1(): OpenApiRoute.() -> Unit = { description = "Return user by id" - tags = listOf("users") + tags = listOf("Users V1") request { pathParameter("user_id") { description = "User id" @@ -172,7 +172,7 @@ fun SwaggerDocument.returnUserByIdV1(): OpenApiRoute.() -> Unit = { */ fun SwaggerDocument.updateUserV1(): OpenApiRoute.() -> Unit = { description = "Changes user by id" - tags = listOf("users") + tags = listOf("Users V1") request { pathParameter("user_id") { description = "User id" diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwaggerV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwaggerV1.kt index 712bbb4a1..6d1160b97 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwaggerV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/swagger/WorkspaceSwaggerV1.kt @@ -15,7 +15,7 @@ import office.effective.dto.WorkspaceZoneDTO */ fun SwaggerDocument.returnWorkspaceByIdV1(): OpenApiRoute.() -> Unit = { description = "Return workspace by id" - tags = listOf("workspaces") + tags = listOf("Workspaces V1") request { pathParameter("id") { description = "Workspace id" @@ -34,12 +34,12 @@ fun SwaggerDocument.returnWorkspaceByIdV1(): OpenApiRoute.() -> Unit = { utilities = listOf( UtilityDTO( id = "50d89406-2bc6-11ee-be56-0242ac120002", - name = "Sockets", + name = "Place", iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", count = 8 ), UtilityDTO( id = "a62a86c6-2bc6-11ee-be56-0242ac120002", - name = "Projectors", + name = "Sockets", iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", count = 1 ) @@ -61,8 +61,10 @@ fun SwaggerDocument.returnWorkspaceByIdV1(): OpenApiRoute.() -> Unit = { * @suppress */ fun SwaggerDocument.returnWorkspaceByTagV1(): OpenApiRoute.() -> Unit = { - description = "Return all workspaces by tag" - tags = listOf("workspaces") + description = "Return all workspaces by tag. Can return workspaces with their bookings " + + "or workspaces that are free during a certain period of time. " + + "Note that requesting workspaces with their bookings significantly increases the response time." + tags = listOf("Workspaces V1") request { queryParameter("workspace_tag") { description = "Workspace tag" @@ -84,8 +86,7 @@ fun SwaggerDocument.returnWorkspaceByTagV1(): OpenApiRoute.() -> Unit = { } queryParameter("with_bookings") { description = "Specify this parameter to get workspaces with their bookings for today. Optional. " + - "Can't be specified together with free_from and free_until. " + - "Requesting workspaces with their bookings significantly increases the response time." + "Can't be specified together with free_from and free_until." example = true required = false allowEmptyValue = false @@ -93,8 +94,7 @@ fun SwaggerDocument.returnWorkspaceByTagV1(): OpenApiRoute.() -> Unit = { queryParameter("with_bookings_from") { description = "Specify this parameter to get workspaces with their bookings for a certain period of time. " + "The default value is the beginning of today. " + - "Should be greater than with_bookings_from. Can't be specified together with free_from and free_until. " + - "Requesting workspaces with their bookings significantly increases the response time." + "Should be greater than with_bookings_from. Can't be specified together with free_from and free_until." example = 1691591501000L required = false allowEmptyValue = false @@ -102,8 +102,7 @@ fun SwaggerDocument.returnWorkspaceByTagV1(): OpenApiRoute.() -> Unit = { queryParameter("with_bookings_until") { description = "Specify this parameter to get workspaces with their bookings for a certain period of time. " + "The default value is the end of today. " + - "Should be greater than with_bookings_from. Can't be specified together with free_from and free_until. " + - "Requesting workspaces with their bookings significantly increases the response time." + "Should be greater than with_bookings_from. Can't be specified together with free_from and free_until." example = 1691591578000L required = false allowEmptyValue = false @@ -120,12 +119,12 @@ fun SwaggerDocument.returnWorkspaceByTagV1(): OpenApiRoute.() -> Unit = { utilities = listOf( UtilityDTO( id = "50d89406-2bc6-11ee-be56-0242ac120002", - name = "Sockets", + name = "Place", iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", count = 8 ), UtilityDTO( id = "a62a86c6-2bc6-11ee-be56-0242ac120002", - name = "Projectors", + name = "Sockets", iconUrl = "https://img.freepik.com/free-photo/beautiful-shot-of-a-white-british-shorthair-kitten_181624-57681.jpg", count = 1 ) @@ -146,8 +145,7 @@ fun SwaggerDocument.returnWorkspaceByTagV1(): OpenApiRoute.() -> Unit = { } } HttpStatusCode.BadRequest to { - description = "Tag shouldn't be null, free_from " + - "and free_until should be numbers from 0 to 2147483647000 (max timestamp)" + description = "Invalid request parameter" } HttpStatusCode.NotFound to { description = "Provided tag doesn't exist" @@ -160,7 +158,7 @@ fun SwaggerDocument.returnWorkspaceByTagV1(): OpenApiRoute.() -> Unit = { */ fun SwaggerDocument.returnAllZonesV1(): OpenApiRoute.() -> Unit = { description = "Returns all workspace zones" - tags = listOf("workspaces") + tags = listOf("Workspaces V1") response { HttpStatusCode.OK to { description = "Returns all workspaces found by tag" From 523eace8086b5ca4a0fe2ce183bdb0b6cff012ae Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sat, 20 Apr 2024 19:15:53 +0600 Subject: [PATCH 14/26] [~] fix kdoc --- .../src/main/kotlin/office/effective/dto/BookingDTO.kt | 7 +++++++ .../effective/features/booking/facade/BookingFacadeV1.kt | 4 ++-- .../features/workspace/facade/WorkspaceFacadeV1.kt | 6 ++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingDTO.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingDTO.kt index 5b6e28ef6..6ffcddc08 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingDTO.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/dto/BookingDTO.kt @@ -4,6 +4,13 @@ import kotlinx.serialization.Serializable import model.RecurrenceDTO @Serializable +@Deprecated( + message = "Deprecated since 1.0 api version", + replaceWith = ReplaceWith( + expression = "BookingRequestDTO or BookingResponseDTO", + imports = ["office.effective.dto.BookingRequestDTO"] + ) +) data class BookingDTO ( val owner: UserDTO, val participants: List, diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt index a706bf5e7..8123a23bd 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt @@ -6,7 +6,6 @@ import office.effective.common.exception.InstanceNotFoundException import office.effective.common.utils.DatabaseTransactionManager import office.effective.common.utils.UuidValidator import office.effective.features.booking.converters.BookingDtoModelConverter -import office.effective.dto.BookingDTO import office.effective.dto.BookingRequestDTO import office.effective.dto.BookingResponseDTO import office.effective.model.Booking @@ -65,7 +64,7 @@ class BookingFacadeV1( * @param bookingRangeFrom lower bound (exclusive) for a endBooking to filter by. * Should be lover than [bookingRangeFrom]. Default value: [BookingConstants.MIN_SEARCH_START_TIME] * @param returnInstances return recurring bookings as non-recurrent instances - * @return [BookingDTO] list + * @return [BookingResponseDTO] list * @author Daniil Zavyalov */ fun findAll( @@ -112,6 +111,7 @@ class BookingFacadeV1( * Updates a given booking. Use the returned model for further operations * * @param bookingDTO changed booking + * @param bookingId booking id * @return updated booking * @author Daniil Zavyalov */ diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt index 089111343..07cffce7f 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt @@ -49,10 +49,12 @@ class WorkspaceFacadeV1( } /** - * Returns all [WorkspaceDTO] with the given tag + * Returns all [WorkspaceDTO] with their bookings by tag * * @param tag tag name of requested workspaces - * @return List of [WorkspaceDTO] with the given [tag] + * @param withBookingsFrom lower bound (exclusive) for a booking.endBooking to filter by. + * @param withBookingsUntil upper bound (exclusive) for a booking.beginBooking to filter by. + * @return List of [WorkspaceDTO] with their bookings * @author Daniil Zavyalov */ fun findAllByTag( From 1ec52e880e60519f9fcac0f4db2109b17edad97c Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sat, 20 Apr 2024 21:59:19 +0600 Subject: [PATCH 15/26] [~] refactor regular workspace booking --- .../common/constants/BookingConstants.kt | 2 +- .../converters/GoogleCalendarConverter.kt | 7 +- .../features/booking/di/BookingDiModule.kt | 2 +- .../repository/BookingMeetingRepository.kt | 20 +- .../repository/BookingRegularRepository.kt | 225 +++++++++++------- 5 files changed, 161 insertions(+), 95 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt index 39e05c004..0bd9e9d71 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt @@ -16,7 +16,7 @@ object BookingConstants { val DEFAULT_CALENDAR: String = System.getenv("DEFAULT_CALENDAR") ?: config.propertyOrNull("calendar.defaultCalendar")?.getString() ?: throw Exception("Environment and config file does not contain Google default calendar id") - val WORKSPACE_CALENDAR: String = System.getenv("WORKSPACE_CALENDAR") + val REGULAR_WORKSPACES_CALENDAR: String = System.getenv("WORKSPACE_CALENDAR") ?: config.propertyOrNull("calendar.workspaceCalendar")?.getString() ?: throw Exception("Environment and config file does not contain workspace Google calendar id") val DEFAULT_TIMEZONE_ID: String = config.propertyOrNull("calendar.defaultTimezone")?.getString() diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt index 86924832b..40159b3a1 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt @@ -183,10 +183,14 @@ class GoogleCalendarConverter( */ fun toGoogleWorkspaceRegularEvent(model: Booking): Event { logger.debug("[toGoogleWorkspaceRegularEvent] converting regular workspace booking to calendar event") + val attendeeList: MutableList = participantsAndOwnerToAttendees(model) + val event = Event().apply { + id = model.id summary = eventSummaryForRegularBooking(model) description = eventDescriptionRegularBooking(model) - getRecurrenceFromRecurrenceModel(model)?.let{ recurrence = it } + attendees = attendeeList + recurrence = getRecurrenceFromRecurrenceModel(model) start = model.beginBooking.toGoogleDateTime() end = model.endBooking.toGoogleDateTime() } @@ -218,6 +222,7 @@ class GoogleCalendarConverter( attendeeList.add(workspaceModelToAttendee(model.workspace)) val event = Event().apply { + id = model.id summary = eventSummaryForMeetingBooking(model) description = eventDescriptionMeetingBooking(model) organizer = userModelToGoogleOrganizer(model.owner) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt index 8fce1b7dc..05e4a76d5 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/di/BookingDiModule.kt @@ -14,7 +14,7 @@ import org.koin.dsl.module val bookingDiModule = module(createdAtStart = true) { single { BookingRepositoryConverter(get(), get(), get(), get()) } single { GoogleCalendarConverter(get(), get(), get(), get()) } - single { BookingRegularRepository(get(), get(), get(), get()) } + single { BookingRegularRepository(get(), get()) } single { BookingMeetingRepository(get(), get(), get(), get()) } single { BookingService(get(), get(), get(), get()) } single { BookingDtoModelConverter(get(), get(), get(), get(), get()) } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt index fe98e9f80..9fdcf7fec 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt @@ -374,7 +374,6 @@ class BookingMeetingRepository( .also { savedBooking -> logger.trace("[save] saved booking: {}", savedBooking) } - } /** @@ -438,18 +437,19 @@ class BookingMeetingRepository( */ private fun updateSingleEvent(booking: Booking, workspaceCalendar: String): Event { val eventOnUpdate = googleCalendarConverter.toGoogleWorkspaceMeetingEvent(booking) + if (singleEventHasCollision(eventOnUpdate, workspaceCalendar)) { throw WorkspaceUnavailableException("Workspace ${booking.workspace.name} " + "unavailable at time between ${booking.beginBooking} and ${booking.endBooking}") } - val eventId = eventOnUpdate.id - val prevEventVersion = findByCalendarIdAndBookingId(eventId) ?: throw InstanceNotFoundException( - WorkspaceBookingEntity::class, "Booking with id $eventId not wound" + val bookingId = booking.id ?: throw MissingIdException("Booking model must have an id for update request") + val prevEventVersion = findByCalendarIdAndBookingId(bookingId) ?: throw InstanceNotFoundException( + WorkspaceBookingEntity::class, "Booking with id $bookingId not wound" ) logger.trace("[updateSingleEvent] previous version of event: {}", prevEventVersion) - return calendarEvents.update(defaultCalendar, eventId, eventOnUpdate).execute() + return calendarEvents.update(defaultCalendar, bookingId, eventOnUpdate).execute() } /** @@ -478,9 +478,10 @@ class BookingMeetingRepository( } /** - * Checks weather the saved event has collision with other events. + * Checks whether a non-recurring event has a collision with other events. * * @param eventToVerify event for collision check + * @return True if event has a collision * */ private fun singleEventHasCollision(eventToVerify: Event, workspaceCalendar: String): Boolean { val sameTimeEvents = basicQuery( @@ -499,11 +500,10 @@ class BookingMeetingRepository( } /** - * Launch checkSingleEventCollision for non-cycle event - * or receive instances for recurrent event and checks all instances. + * Checks whether the saved recurring event has a collision with other events. * - * @param incomingEvent: [Event] - Must take only SAVED event - * @return Boolean. True if booking available + * @param incomingEvent must take only SAVED event + * @return True if event has a collision and should be deleted * */ private fun recurringEventHasCollision(incomingEvent: Event, workspaceCalendar: String): Boolean { logger.debug( diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt index 0336b9bab..f909b8290 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt @@ -9,9 +9,6 @@ import office.effective.common.exception.InstanceNotFoundException import office.effective.common.exception.MissingIdException import office.effective.common.exception.WorkspaceUnavailableException import office.effective.features.booking.converters.GoogleCalendarConverter -import office.effective.features.user.repository.UserRepository -import office.effective.features.workspace.repository.WorkspaceEntity -import office.effective.features.workspace.repository.WorkspaceRepository import office.effective.model.Booking import org.slf4j.LoggerFactory import java.time.Instant @@ -25,11 +22,9 @@ import java.util.* class BookingRegularRepository( private val calendar: Calendar, private val googleCalendarConverter: GoogleCalendarConverter, - private val workspaceRepository: WorkspaceRepository, - private val userRepository: UserRepository ) : IBookingRepository { private val calendarEvents = calendar.Events() - private val workspaceCalendar: String = BookingConstants.WORKSPACE_CALENDAR + private val regularWorkspacesCalendar: String = BookingConstants.REGULAR_WORKSPACES_CALENDAR private val logger = LoggerFactory.getLogger(this::class.java) /** @@ -54,7 +49,7 @@ class BookingRegularRepository( override fun deleteById(id: String) { logger.debug("[deleteById] deleting the booking with id={}", id) try { - calendarEvents.delete(workspaceCalendar, id).execute() + calendarEvents.delete(regularWorkspacesCalendar, id).execute() } catch (e: GoogleJsonResponseException) { if (e.statusCode != 404 && e.statusCode != 410) { throw e @@ -87,7 +82,7 @@ class BookingRegularRepository( * or null if event with the given id doesn't exist * @author Daniil Zavyalov */ - private fun findByCalendarIdAndBookingId(bookingId: String, calendarId: String = workspaceCalendar): Event? { + private fun findByCalendarIdAndBookingId(bookingId: String, calendarId: String = regularWorkspacesCalendar): Event? { return try { calendar.events().get(calendarId, bookingId).execute() } catch (e: GoogleJsonResponseException) { @@ -110,7 +105,7 @@ class BookingRegularRepository( timeMin: Long, timeMax: Long? = null, singleEvents: Boolean = true, - calendarId: String = workspaceCalendar + calendarId: String = regularWorkspacesCalendar ): Calendar.Events.List { return calendarEvents.list(calendarId) .setSingleEvents(singleEvents) @@ -252,28 +247,50 @@ class BookingRegularRepository( * * @param booking [Booking] to be saved * @return saved [Booking] - * @author Daniil Zavyalov, Danil Kiselev */ override fun save(booking: Booking): Booking { - logger.debug("[save] saving booking of workspace with id {}", booking.workspace.id) + logger.debug("[save] saving booking of regular workspace with id {}", booking.workspace.id) + logger.trace("[save] regular workspace booking to save: {}", booking) + val savedEvent: Event = if (booking.recurrence != null) { + saveRecurringEvent(booking) + } else { + saveSingleEvent(booking) + } + return googleCalendarConverter.toWorkspaceBooking(savedEvent) + .also { savedBooking -> + logger.trace("[save] saved booking: {}", savedBooking) + } + } + + /** + * Saving booking without recurrence. Checks collision before saving an event. + */ + private fun saveSingleEvent(booking: Booking): Event { val workspaceId = booking.workspace.id ?: throw MissingIdException("Missing booking workspace id") - val event = googleCalendarConverter.toGoogleWorkspaceRegularEvent(booking) + val event = googleCalendarConverter.toGoogleWorkspaceMeetingEvent(booking) + if (singleEventHasCollision(event, workspaceId)) { + throw WorkspaceUnavailableException("Workspace ${booking.workspace.name} " + + "unavailable at time between ${booking.beginBooking} and ${booking.endBooking}") + } + return calendar.Events().insert(regularWorkspacesCalendar, event).execute() + } - logger.trace("[save] booking to save: {}", event) - val savedEvent = calendar.Events().insert(workspaceCalendar, event).execute() - logger.trace("[save] event inserted") + /** + * Saving booking with recurrence. Checks collision for all event instances after its saving. + * + * @param booking updated booking to be saved + */ + private fun saveRecurringEvent(booking: Booking): Event { + val workspaceId = booking.workspace.id ?: throw MissingIdException("Missing booking workspace id") + val event = googleCalendarConverter.toGoogleWorkspaceMeetingEvent(booking) - if (checkBookingAvailable(savedEvent, workspaceId)) { - val savedBooking = googleCalendarConverter.toWorkspaceBooking(savedEvent) - logger.trace("[save] saved booking: {}", savedBooking) - return savedBooking - } else { + val savedEvent = calendar.Events().insert(regularWorkspacesCalendar, event).execute() + + if (recurringEventHasCollision(savedEvent, workspaceId)) { deleteById(savedEvent.id) - throw WorkspaceUnavailableException( - "Workspace ${booking.workspace.name} " + - "unavailable at time between ${booking.beginBooking} and ${booking.endBooking}" - ) + throw WorkspaceUnavailableException("Workspace ${booking.workspace.name} unavailable at specified time.") } + return savedEvent } /** @@ -281,91 +298,135 @@ class BookingRegularRepository( * * @param booking changed booking * @return [Booking] after change saving - * @throws MissingIdException if [Booking.id] is null + * @throws MissingIdException if [Booking.id] or [Booking.workspace].id is null * @throws InstanceNotFoundException if booking given id doesn't exist in the database - * @author Daniil Zavyalov, Danil Kiselev + * @throws WorkspaceUnavailableException if booking unavailable because of collision check */ override fun update(booking: Booking): Booking { logger.debug("[update] updating booking of workspace with id {}", booking.id) + logger.trace("[update] new booking: {}", booking) + + val updatedEvent: Event = if (booking.recurrence != null) { + updateRecurringEvent(booking) + } else { + updateSingleEvent(booking) + } + return googleCalendarConverter.toBookingModelForMeetingWorkspace(updatedEvent) + .also { updatedBooking -> + logger.trace("[update] updated booking: {}", updatedBooking) + } + } + + /** + * Updating booking without recurrence. Checks collision before updating an event. + */ + private fun updateSingleEvent(booking: Booking): Event { val workspaceId = booking.workspace.id ?: throw MissingIdException("Missing booking workspace id") + val eventOnUpdate = googleCalendarConverter.toGoogleWorkspaceRegularEvent(booking) + if (singleEventHasCollision(eventOnUpdate, workspaceId)) { + throw WorkspaceUnavailableException("Workspace ${booking.workspace.name} " + + "unavailable at time between ${booking.beginBooking} and ${booking.endBooking}") + } + + val bookingId = booking.id ?: throw MissingIdException("Booking model must have an id for update request") + val prevEventVersion = findByCalendarIdAndBookingId(bookingId) ?: throw InstanceNotFoundException( + WorkspaceBookingEntity::class, "Booking with id $bookingId not wound" + ) + logger.trace("[updateSingleEvent] previous version of event: {}", prevEventVersion) + + return calendarEvents.update(regularWorkspacesCalendar, bookingId, eventOnUpdate).execute() + } + + /** + * Updating booking with recurrence. Checks collision for all event instances after its update. + * + * @param booking updated booking to be saved + */ + private fun updateRecurringEvent(booking: Booking): Event { val bookingId = booking.id ?: throw MissingIdException("Update model must have id") - val previousVersionOfEvent = findByCalendarIdAndBookingId(bookingId) ?: throw InstanceNotFoundException( + val workspaceId = booking.workspace.id ?: throw MissingIdException("Missing booking workspace id") + val prevEventVersion = findByCalendarIdAndBookingId(bookingId) ?: throw InstanceNotFoundException( WorkspaceBookingEntity::class, "Booking with id $bookingId not wound" ) - logger.trace("[update] previous version of event: {}", previousVersionOfEvent) + logger.trace("[updateRecurringEvent] previous version of event: {}", prevEventVersion) val eventOnUpdate = googleCalendarConverter.toGoogleWorkspaceRegularEvent(booking) - logger.trace("[update] new version of event: {}", eventOnUpdate) - val updatedEvent: Event = calendarEvents.update(workspaceCalendar, bookingId, eventOnUpdate).execute() - logger.trace("[update] event updated") + val updatedEvent: Event = calendarEvents.update(regularWorkspacesCalendar, bookingId, eventOnUpdate).execute() val sequence = updatedEvent.sequence - if (checkBookingAvailable(updatedEvent, workspaceId)) { - val updatedBooking = googleCalendarConverter.toWorkspaceBooking(updatedEvent) - logger.trace("[update] updated booking: {}", updatedBooking) - return updatedBooking - } else { - previousVersionOfEvent.sequence = sequence - calendarEvents.update(workspaceCalendar, bookingId, previousVersionOfEvent).execute() - throw WorkspaceUnavailableException( - "Workspace ${booking.workspace.name} " + - "unavailable at time between ${booking.beginBooking} and ${booking.endBooking}" - ) + if (recurringEventHasCollision(updatedEvent, workspaceId)) { + prevEventVersion.sequence = sequence + calendarEvents.update(regularWorkspacesCalendar, bookingId, prevEventVersion).execute() + throw WorkspaceUnavailableException("Workspace ${booking.workspace.name} unavailable at specified time.") } + return updatedEvent } /** - * Launch checkSingleEventCollision for non-cycle event or - * receive instances for recurrent event and checks all instances. + * Checks whether a non-recurring event has a collision with other events. * - * @param incomingEvent: [Event] - Must take only SAVED event - * @return Boolean. True if booking available - * @author Kiselev Danil, Daniil Zavyalov + * @param eventToVerify event for collision check + * @return True if event has a collision and can't be saved * */ - private fun checkBookingAvailable(incomingEvent: Event, workspaceId: UUID): Boolean { - logger.debug( - "[checkBookingAvailable] checking if workspace with id={} available for event {}", - workspaceId, - incomingEvent - ) - var isAvailable = false; - - if (incomingEvent.recurrence != null) { - //TODO: Check, if we can receive instances without pushing this event into calendar - val instances = calendarEvents.instances(workspaceCalendar, incomingEvent.id).execute().items - for (instance in instances) { - if (!checkSingleEventCollision(instance, workspaceId)) { - return false - } else { - isAvailable = true - } + private fun singleEventHasCollision(eventToVerify: Event, workspaceId: UUID): Boolean { + val sameTimeEvents = basicQuery( + timeMin = eventToVerify.start.dateTime.value, + timeMax = eventToVerify.end.dateTime.value, + singleEvents = true, + ).setQ(workspaceId.toString()) + .execute().items + for (event in sameTimeEvents) { + if (areEventsOverlap(eventToVerify, event) && eventsIsNotSame(eventToVerify, event)) { + return true } - } else { - isAvailable = checkSingleEventCollision(incomingEvent, workspaceId) } - logger.debug("[checkBookingAvailable] result {}", true) - return isAvailable + return false } /** - * Contains collision condition. Checks collision between single event from param - * and all saved events from [Event.start] until [Event.end] + * Checks whether the saved recurring event has a collision with other events. * - * @param event: [Event] - Must take only SAVED event - * @author Kiselev Danil, Daniil Zavyalov + * @param incomingEvent must take only SAVED event + * @param workspaceId id of recurring workspace + * @return True if event has a collision and should be deleted * */ - private fun checkSingleEventCollision(event: Event, workspaceId: UUID): Boolean { - val events = basicQuery(event.start.dateTime.value, event.end.dateTime.value) - .setQ(workspaceId.toString()) + private fun recurringEventHasCollision(incomingEvent: Event, workspaceId: UUID): Boolean { + logger.debug( + "[checkBookingAvailable] checking if workspace with calendar id={} available for event {}", + regularWorkspacesCalendar, + incomingEvent + ) + + var result = false + //TODO: Check, if we can receive instances without pushing this event into calendar + val instances = calendarEvents.instances(regularWorkspacesCalendar, incomingEvent.id) + .setMaxResults(50) .execute().items - for (i in events) { - if ( - !((i.start.dateTime.value >= event.end.dateTime.value) || (i.end.dateTime.value <= event.start.dateTime.value)) - && (i.id != event.id) - ) { - return false + + for (instance in instances) { + if (singleEventHasCollision(instance, workspaceId)) { + result = true } } - return true + logger.debug("[recurringEventHasCollision] result: {}", result) + return result + } + + /** + * Checks whether events has collision + */ + private fun areEventsOverlap(firstEvent: Event, secondEvent: Event): Boolean { + return secondEvent.start.dateTime.value < firstEvent.end.dateTime.value + && secondEvent.end.dateTime.value > firstEvent.start.dateTime.value + } + + /** + * Checks whether events aren't the same event or instances of the same event + */ + private fun eventsIsNotSame(firstEvent: Event, secondEvent: Event): Boolean { + return firstEvent.id != secondEvent.id && + firstEvent.id != secondEvent.recurringEventId && + firstEvent.recurringEventId != secondEvent.id && + (firstEvent.recurringEventId != firstEvent.recurringEventId || firstEvent.recurringEventId == null) } } From bdbc153e3b06ab557b6a931ca1975f7c13f0bb44 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sat, 20 Apr 2024 22:51:44 +0600 Subject: [PATCH 16/26] [~] refactor converter --- .../converters/GoogleCalendarConverter.kt | 250 +++++++++--------- .../repository/BookingMeetingRepository.kt | 17 +- .../repository/BookingRegularRepository.kt | 18 +- 3 files changed, 149 insertions(+), 136 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt index 40159b3a1..0eefde2ed 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt @@ -11,9 +11,7 @@ import office.effective.dto.BookingDTO import office.effective.dto.UserDTO import office.effective.dto.WorkspaceDTO import office.effective.features.calendar.repository.CalendarIdsRepository -import office.effective.features.user.converters.UserDTOModelConverter import office.effective.features.user.repository.UserRepository -import office.effective.features.workspace.converters.WorkspaceFacadeConverter import office.effective.model.Booking import office.effective.model.UserModel import office.effective.model.Workspace @@ -39,118 +37,129 @@ class GoogleCalendarConverter( private val defaultAccount: String = BookingConstants.DEFAULT_CALENDAR /** - * Gets the list of event participants, excluding resources, and returns a list of user Models. + * Converts regular [Event] to [Booking] * - * @param event The event for which participants need to be retrieved. - * @return List of user Models. - */ - private fun getParticipantsModels(event: Event): List { - val attendees = event.attendees - .filter { attendee -> !attendee.isResource } - .map { attendee -> attendee.email } - return getAllUserModels(attendees) - } - - /** - * Retrieves a list of users by email addresses and converts them to a list of user Models. + * Creates placeholders if workspace or owner doesn't exist in database * - * @param emails List of user email addresses. - * @return List of user Models. + * @param event [Event] to be converted + * @return The resulting [Booking] object */ - private fun getAllUserModels(emails: List): List { - return userRepository.findAllByEmails(emails) - } + fun toRegularWorkspaceBooking(event: Event): Booking { + logger.debug("[toRegularWorkspaceBooking] converting an event to workspace booking dto") + val recurrence = event.recurrence?.toString()?.getRecurrence() - /** - * Retrieves the calendar ID of the workspace from the event. - * If the ID is not found, returns a default value with a warning log. - * - * @param event The event from which to retrieve the calendar ID. - * @return Calendar ID of the workspace or default value. - */ - private fun getCalendarId(event: Event): String? { - return event.attendees?.firstOrNull { it?.resource ?: false } - ?.email + val model = Booking( + owner = getUserModel(event), + participants = emptyList(), + workspace = getWorkspaceModel(event), + id = event.id ?: null, + beginBooking = Instant.ofEpochMilli(event.start?.dateTime?.value ?: 0), + endBooking = Instant.ofEpochMilli(event.end?.dateTime?.value ?: 1), + recurrence = recurrence?.let { RecurrenceConverter.recurrenceToModel(it) }, + recurringBookingId = event.recurringEventId + ) + logger.trace("[toRegularWorkspaceBooking] {}", model.toString()) + return model } /** - * Converts regular [Event] to [Booking] - * - * Creates placeholders if workspace or owner doesn't exist in database + * Find [UserModel]. May return placeholder if user with the given email doesn't exist in database * - * @param event [Event] to be converted - * @return The resulting [Booking] object - * @throws Exception if it fails to get user id from description or workspace id from summary of Google Calendar event + * @param event + * @return [UserModel] with data from database or [UserModel] placeholder * @author Danil Kiselev, Max Mishenko, Daniil Zavyalov */ - fun toWorkspaceBooking(event: Event): Booking { - logger.debug("[toWorkspaceBooking] converting an event to workspace booking dto") + private fun getUserModel(event: Event): UserModel { val userId: UUID = try { UUID.fromString(event.description.substringBefore(" ")) } catch (e: Exception) { - throw Exception("Can't get user UUID from Google Calendar event description. Reason: ${e.printStackTrace()}") + logger.error("[getUserModel] Can't get user UUID from Google Calendar event description. " + + "Creating placeholder", e) + return UserModel( + id = null, + fullName = "Unregistered user", + tag = null, + active = false, + role = null, + avatarURL = null, + integrations = emptySet(), + email = "placeholder@gmail.com" + ) } - val workspaceID: UUID = try { + val userModel: UserModel = userRepository.findById(userId) + ?: run { + logger.warn("[getUserModel] can't find a user with id ${userId}. Creating placeholder.") + UserModel( + id = null, + fullName = "Unregistered user", + tag = null, + active = false, + role = null, + avatarURL = null, + integrations = emptySet(), + email = "placeholder@gmail.com" + ) + } + return userModel + } + + /** + * Find [Workspace] for regular workspace booking. May return placeholder + * + * @param event + * @return [Workspace] + */ + private fun getWorkspaceModel(event: Event): Workspace { + val workspaceId: UUID = try { UUID.fromString(event.summary.substringBefore(" ")) } catch (e: Exception) { - throw Exception("Can't get user UUID from Google Calendar event summary. Reason: ${e.printStackTrace()}") + logger.error("[getWorkspaceModel] Can't get workspace UUID from Google Calendar event description. " + + "Creating placeholder", e) + return Workspace(null, "Nonexistent workspace", "regular", listOf(), null) + } + return workspaceRepository.findById(workspaceId) ?: run { + logger.warn("[getWorkspaceModel] can't find a workspace with id ${workspaceId}. Creating placeholder.") + Workspace(null, "Nonexistent workspace", "regular", listOf(), null) } - val recurrence = event.recurrence?.toString()?.getRecurrence() - - val model = Booking( - owner = userRepository.findById(userId) - ?: run { - logger.warn("[toWorkspaceBooking] can't find user with id ${userId}. Creating placeholder.") - UserModel( - id = null, - fullName = "Nonexistent user", - tag = null, - active = false, - role = null, - avatarURL = null, - integrations = emptySet(), - email = "" - ) - }, - participants = emptyList(), - workspace = workspaceRepository.findById(workspaceID) - ?: run { - logger.warn("[toWorkspaceBooking] can't find a user with id ${userId}. Creating placeholder.") - Workspace(null, "Nonexistent workspace", "placeholder", listOf(), null) - }, - id = event.id ?: null, - beginBooking = Instant.ofEpochMilli(event.start?.dateTime?.value ?: 0), - endBooking = Instant.ofEpochMilli( - event.end?.dateTime?.value ?: ((event.start?.dateTime?.value ?: 0) + 86400000) - ), - recurrence = recurrence?.toDto()?.let { - RecurrenceConverter.recurrenceToModel(recurrence) - } - ) - logger.trace("[toBookingDTO] {}", model.toString()) - return model } /** - * Find [WorkspaceDTO] by workspace calendar id + * Converts meeting [Event] to [Booking] * - * @param calendarId Google id of calendar of workspace - * @return [WorkspaceDTO] + * @param event [Event] to be converted + * @return The resulting [BookingDTO] object * @author Danil Kiselev, Max Mishenko */ - private fun getWorkspaceModel(calendarId: String?): Workspace { - if (calendarId == null) { - logger.warn("[toBookingDTO] can't get workspace calendar from event.attendees") - return Workspace(null, "placeholder", "placeholder", listOf(), null) + fun toMeetingWorkspaceBooking(event: Event): Booking { + logger.debug("[toGoogleEvent] converting calendar event to meeting room booking model") + val organizer: String = event.organizer?.email ?: "" + val email = if (organizer != defaultAccount) { + logger.trace("[toBookingModel] organizer email derived from event.organizer field") + organizer + } else { + logger.trace("[toBookingModel] organizer email derived from event description") + event.description?.substringBefore(" ") ?: "" } - return calendarIdsRepository.findWorkspaceById(calendarId) //may return placeholder + val recurrence = event.recurrence?.toString()?.getRecurrence() + + val booking = Booking( + owner = getUserModel(email), + participants = getParticipantsModels(event), + workspace = getWorkspaceModel(getCalendarId(event)), + id = event.id ?: null, + beginBooking = Instant.ofEpochMilli(event.start?.dateTime?.value ?: 0), + endBooking = Instant.ofEpochMilli(event.end?.dateTime?.value ?: 1), + recurrence = recurrence?.let { RecurrenceConverter.recurrenceToModel(it) } + ) + logger.trace("[toBookingModel] {}", booking.toString()) + return booking } /** * Find [UserModel] by email. May return placeholder if user with the given email doesn't exist in database * * @param email - * @return [UserDTO] with data from database or [UserDTO] placeholder with the given [email] + * @return [UserModel] with data from database or [UserModel] placeholder with the given [email] * @author Danil Kiselev, Max Mishenko, Daniil Zavyalov */ private fun getUserModel(email: String): UserModel { @@ -171,6 +180,45 @@ class GoogleCalendarConverter( return userModel } + /** + * Gets the list of event participants, excluding resources, and returns a list of user Models. + * + * @param event The event for which participants need to be retrieved. + * @return List of user Models. + */ + private fun getParticipantsModels(event: Event): List { + val attendees = event.attendees + .filter { attendee -> !attendee.isResource } + .map { attendee -> attendee.email } + return userRepository.findAllByEmails(attendees) + } + + /** + * Retrieves the calendar ID of the workspace from the event. + * If the ID is not found, returns a default value with a warning log. + * + * @param event The event from which to retrieve the calendar ID. + * @return Calendar ID of the workspace or default value. + */ + private fun getCalendarId(event: Event): String? { + return event.attendees?.firstOrNull { it?.resource ?: false } + ?.email + } + + /** + * Find [WorkspaceDTO] by workspace calendar id + * + * @param calendarId Google id of calendar of workspace + * @return [WorkspaceDTO] + * @author Danil Kiselev, Max Mishenko + */ + private fun getWorkspaceModel(calendarId: String?): Workspace { + if (calendarId == null) { + logger.warn("[toBookingDTO] can't get workspace calendar from event.attendees") + return Workspace(null, "placeholder", "placeholder", listOf(), null) + } + return calendarIdsRepository.findWorkspaceById(calendarId) //may return placeholder + } /** * Converts regular workspace [Booking] to [Event]. [Event.description] is used to indicate the booking author, @@ -267,40 +315,6 @@ class GoogleCalendarConverter( return attendees } - /** - * Converts meeting [Event] to [Booking] - * - * - * @param event [Event] to be converted - * @return The resulting [BookingDTO] object - * @author Danil Kiselev, Max Mishenko - */ - fun toBookingModelForMeetingWorkspace(event: Event): Booking { - logger.debug("[toGoogleEvent] converting calendar event to meeting room booking model") - val organizer: String = event.organizer?.email ?: "" - val email = if (organizer != defaultAccount) { - logger.trace("[toBookingModel] organizer email derived from event.organizer field") - organizer - } else { - logger.trace("[toBookingModel] organizer email derived from event description") - event.description?.substringBefore(" ") ?: "" - } - val recurrence = event.recurrence?.toString()?.getRecurrence() - - val booking = Booking( - owner = getUserModel(email), - participants = getParticipantsModels(event), - workspace = getWorkspaceModel(getCalendarId(event)), - id = event.id ?: null, - beginBooking = Instant.ofEpochMilli(event.start?.dateTime?.value ?: 0), - endBooking = Instant.ofEpochMilli(event.end?.dateTime?.value ?: 1), - recurrence = recurrence?.let { RecurrenceConverter.recurrenceToModel(it) } - ) - logger.trace("[toBookingModel] {}", booking.toString()) - return booking - - } - /** * Converts [Instant] to [EventDateTime] * diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt index 9fdcf7fec..4f78d2e98 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt @@ -56,8 +56,7 @@ class BookingMeetingRepository( */ override fun existsById(id: String): Boolean { logger.debug("[existsById] checking whether a booking with id={} exists", id) - val event: Any? = findByCalendarIdAndBookingId(id) - return event != null + return findByCalendarIdAndBookingId(id) != null } /** @@ -86,7 +85,7 @@ class BookingMeetingRepository( override fun findById(bookingId: String): Booking? { logger.debug("[findById] retrieving a booking with id={}", bookingId) val event: Event? = findByCalendarIdAndBookingId(bookingId) - return event?.let { googleCalendarConverter.toBookingModelForMeetingWorkspace(it) } + return event?.let { googleCalendarConverter.toMeetingWorkspaceBooking(it) } } /** @@ -158,7 +157,7 @@ class BookingMeetingRepository( val eventsWithWorkspace = basicQuery(eventRangeFrom, eventRangeTo, returnInstances, workspaceCalendarId) .execute().items - return eventsWithWorkspace.toList().map { googleCalendarConverter.toBookingModelForMeetingWorkspace(it) } + return eventsWithWorkspace.toList().map { googleCalendarConverter.toMeetingWorkspaceBooking(it) } } /** @@ -280,7 +279,7 @@ class BookingMeetingRepository( val result = mutableListOf() for (event in eventsWithUser) { if (checkEventOrganizer(event, userEmail)) { - result.add(googleCalendarConverter.toBookingModelForMeetingWorkspace(event)) + result.add(googleCalendarConverter.toMeetingWorkspaceBooking(event)) } else { logger.trace("[findAllByOwnerId] filtered out event: {}", event) } @@ -323,7 +322,7 @@ class BookingMeetingRepository( val result = mutableListOf() for (event in eventsWithUserAndWorkspace) { if (checkEventOrganizer(event, userEmail)) { - result.add(googleCalendarConverter.toBookingModelForMeetingWorkspace(event)) + result.add(googleCalendarConverter.toMeetingWorkspaceBooking(event)) } else { logger.trace("[findAllByOwnerAndWorkspaceId] filtered out event: {}", event) } @@ -349,7 +348,7 @@ class BookingMeetingRepository( ) val calendars: List = calendarIdsRepository.findAllCalendarsId() val events: List = getAllEvents(calendars, returnInstances, eventRangeFrom, eventRangeTo) - return events.map { googleCalendarConverter.toBookingModelForMeetingWorkspace(it) } + return events.map { googleCalendarConverter.toMeetingWorkspaceBooking(it) } } /** @@ -370,7 +369,7 @@ class BookingMeetingRepository( } else { saveSingleEvent(booking, workspaceCalendar) } - return googleCalendarConverter.toBookingModelForMeetingWorkspace(savedEvent) + return googleCalendarConverter.toMeetingWorkspaceBooking(savedEvent) .also { savedBooking -> logger.trace("[save] saved booking: {}", savedBooking) } @@ -426,7 +425,7 @@ class BookingMeetingRepository( } else { updateSingleEvent(booking, workspaceCalendar) } - return googleCalendarConverter.toBookingModelForMeetingWorkspace(updatedEvent) + return googleCalendarConverter.toMeetingWorkspaceBooking(updatedEvent) .also { updatedBooking -> logger.trace("[update] updated booking: {}", updatedBooking) } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt index f909b8290..204d62ceb 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt @@ -69,7 +69,7 @@ class BookingRegularRepository( logger.debug("[findById] retrieving a booking with id={}", bookingId) val event: Event? = findByCalendarIdAndBookingId(bookingId) logger.trace("[findById] request to Google Calendar completed") - return event?.let { googleCalendarConverter.toWorkspaceBooking(it) } + return event?.let { googleCalendarConverter.toRegularWorkspaceBooking(it) } } /** @@ -142,7 +142,7 @@ class BookingRegularRepository( logger.trace("[findAllByWorkspaceId] request to Google Calendar completed") return eventsWithWorkspace.map { event -> - googleCalendarConverter.toWorkspaceBooking(event) + googleCalendarConverter.toRegularWorkspaceBooking(event) } } @@ -177,7 +177,7 @@ class BookingRegularRepository( logger.trace("[findAllByOwnerId] request to Google Calendar completed") return eventsWithUser.map { event -> - googleCalendarConverter.toWorkspaceBooking(event) + googleCalendarConverter.toRegularWorkspaceBooking(event) } } @@ -213,7 +213,7 @@ class BookingRegularRepository( logger.trace("[findAllByOwnerAndWorkspaceId] request to Google Calendar completed") return eventsWithUserAndWorkspace.map { event -> - googleCalendarConverter.toWorkspaceBooking(event) + googleCalendarConverter.toRegularWorkspaceBooking(event) } } @@ -237,7 +237,7 @@ class BookingRegularRepository( logger.trace("[findAll] request to Google Calendar completed") return events.map { event -> - googleCalendarConverter.toWorkspaceBooking(event) + googleCalendarConverter.toRegularWorkspaceBooking(event) } } @@ -256,7 +256,7 @@ class BookingRegularRepository( } else { saveSingleEvent(booking) } - return googleCalendarConverter.toWorkspaceBooking(savedEvent) + return googleCalendarConverter.toRegularWorkspaceBooking(savedEvent) .also { savedBooking -> logger.trace("[save] saved booking: {}", savedBooking) } @@ -267,7 +267,7 @@ class BookingRegularRepository( */ private fun saveSingleEvent(booking: Booking): Event { val workspaceId = booking.workspace.id ?: throw MissingIdException("Missing booking workspace id") - val event = googleCalendarConverter.toGoogleWorkspaceMeetingEvent(booking) + val event = googleCalendarConverter.toGoogleWorkspaceRegularEvent(booking) if (singleEventHasCollision(event, workspaceId)) { throw WorkspaceUnavailableException("Workspace ${booking.workspace.name} " + "unavailable at time between ${booking.beginBooking} and ${booking.endBooking}") @@ -282,7 +282,7 @@ class BookingRegularRepository( */ private fun saveRecurringEvent(booking: Booking): Event { val workspaceId = booking.workspace.id ?: throw MissingIdException("Missing booking workspace id") - val event = googleCalendarConverter.toGoogleWorkspaceMeetingEvent(booking) + val event = googleCalendarConverter.toGoogleWorkspaceRegularEvent(booking) val savedEvent = calendar.Events().insert(regularWorkspacesCalendar, event).execute() @@ -311,7 +311,7 @@ class BookingRegularRepository( } else { updateSingleEvent(booking) } - return googleCalendarConverter.toBookingModelForMeetingWorkspace(updatedEvent) + return googleCalendarConverter.toRegularWorkspaceBooking(updatedEvent) .also { updatedBooking -> logger.trace("[update] updated booking: {}", updatedBooking) } From b9ab833b4e3fd8d4ccb966adb118a84204915b8c Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sun, 21 Apr 2024 20:56:13 +0600 Subject: [PATCH 17/26] [~] fix timezone --- .../booking/converters/GoogleCalendarConverter.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt index 0eefde2ed..af0391815 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt @@ -21,6 +21,8 @@ import office.effective.features.workspace.repository.WorkspaceRepository import office.effective.model.RecurrenceModel.Companion.toRecurrence import org.slf4j.LoggerFactory import java.time.Instant +import java.time.ZoneId +import java.time.ZoneOffset import java.util.* import kotlin.collections.List as List @@ -242,7 +244,7 @@ class GoogleCalendarConverter( start = model.beginBooking.toGoogleDateTime() end = model.endBooking.toGoogleDateTime() } - logger.trace("[toGoogleWorkspaceEvent] {}", event.toString()) + logger.trace("[toGoogleWorkspaceEvent] {}", event) return event } @@ -280,7 +282,7 @@ class GoogleCalendarConverter( end = model.endBooking.toGoogleDateTime() } logger.debug("[toGoogleWorkspaceEvent] converting workspace booking model to calendar event") - return event; + return event } private fun eventSummaryForMeetingBooking(model: Booking): String { @@ -322,8 +324,10 @@ class GoogleCalendarConverter( * @author Danil Kiselev, Max Mishenko */ private fun Instant.toGoogleDateTime():EventDateTime { + val timeMillis = this.toEpochMilli() + val millisZoneOffset = TimeZone.getTimeZone(BookingConstants.DEFAULT_TIMEZONE_ID).getOffset(timeMillis) return EventDateTime().also { - it.dateTime = DateTime(this.toEpochMilli()) + it.dateTime = DateTime(timeMillis - millisZoneOffset) it.timeZone = BookingConstants.DEFAULT_TIMEZONE_ID } } From 54f5e5766c58045908d121db8823004b79b49a2f Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sun, 21 Apr 2024 23:23:38 +0600 Subject: [PATCH 18/26] [~] fix date conversion and collision --- .../common/constants/BookingConstants.kt | 6 ++++ .../booking/converters/DateExtensions.kt | 13 +++++++ .../converters/GoogleCalendarConverter.kt | 25 +++++-------- .../repository/BookingMeetingRepository.kt | 36 +++++++++++-------- .../repository/BookingRegularRepository.kt | 27 ++++++++------ 5 files changed, 65 insertions(+), 42 deletions(-) create mode 100644 effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/DateExtensions.kt diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt index 0bd9e9d71..2db157e36 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt @@ -1,6 +1,9 @@ package office.effective.common.constants import office.effective.config +import org.threeten.bp.LocalDateTime +import java.time.Instant +import java.util.* /** * Constants for booking @@ -21,5 +24,8 @@ object BookingConstants { ?: throw Exception("Environment and config file does not contain workspace Google calendar id") val DEFAULT_TIMEZONE_ID: String = config.propertyOrNull("calendar.defaultTimezone")?.getString() ?: throw Exception("Config file does not contain default timezone id") + val DEFAULT_TIMEZONE_OFFSET_MILLIS: Long = TimeZone.getTimeZone(DEFAULT_TIMEZONE_ID) + .getOffset(Instant.now().toEpochMilli()) + .toLong() const val UNTIL_FORMAT = "yyyyMMdd" } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/DateExtensions.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/DateExtensions.kt new file mode 100644 index 000000000..359b99c4b --- /dev/null +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/DateExtensions.kt @@ -0,0 +1,13 @@ +package office.effective.features.booking.converters + +import com.google.api.client.util.DateTime +import office.effective.common.constants.BookingConstants + +/** + * Converts local time to Google [DateTime] in GMT. + * + * Use it for all requests to Google Calendar. + */ +fun Long.toGoogleDateTime(): DateTime { + return DateTime(this - BookingConstants.DEFAULT_TIMEZONE_OFFSET_MILLIS) +} \ No newline at end of file diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt index af0391815..ae1855f1b 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt @@ -1,6 +1,5 @@ package office.effective.features.booking.converters -import com.google.api.client.util.DateTime import com.google.api.services.calendar.model.Event import com.google.api.services.calendar.model.Event.Organizer import com.google.api.services.calendar.model.EventAttendee @@ -8,7 +7,6 @@ import com.google.api.services.calendar.model.EventDateTime import office.effective.common.constants.BookingConstants import office.effective.common.utils.UuidValidator import office.effective.dto.BookingDTO -import office.effective.dto.UserDTO import office.effective.dto.WorkspaceDTO import office.effective.features.calendar.repository.CalendarIdsRepository import office.effective.features.user.repository.UserRepository @@ -21,8 +19,6 @@ import office.effective.features.workspace.repository.WorkspaceRepository import office.effective.model.RecurrenceModel.Companion.toRecurrence import org.slf4j.LoggerFactory import java.time.Instant -import java.time.ZoneId -import java.time.ZoneOffset import java.util.* import kotlin.collections.List as List @@ -241,8 +237,8 @@ class GoogleCalendarConverter( description = eventDescriptionRegularBooking(model) attendees = attendeeList recurrence = getRecurrenceFromRecurrenceModel(model) - start = model.beginBooking.toGoogleDateTime() - end = model.endBooking.toGoogleDateTime() + start = model.beginBooking.toGoogleEventDateTime() + end = model.endBooking.toGoogleEventDateTime() } logger.trace("[toGoogleWorkspaceEvent] {}", event) return event @@ -278,8 +274,8 @@ class GoogleCalendarConverter( organizer = userModelToGoogleOrganizer(model.owner) attendees = attendeeList recurrence = getRecurrenceFromRecurrenceModel(model) - start = model.beginBooking.toGoogleDateTime() - end = model.endBooking.toGoogleDateTime() + start = model.beginBooking.toGoogleEventDateTime() + end = model.endBooking.toGoogleEventDateTime() } logger.debug("[toGoogleWorkspaceEvent] converting workspace booking model to calendar event") return event @@ -321,15 +317,12 @@ class GoogleCalendarConverter( * Converts [Instant] to [EventDateTime] * * @return [EventDateTime] - * @author Danil Kiselev, Max Mishenko */ - private fun Instant.toGoogleDateTime():EventDateTime { - val timeMillis = this.toEpochMilli() - val millisZoneOffset = TimeZone.getTimeZone(BookingConstants.DEFAULT_TIMEZONE_ID).getOffset(timeMillis) - return EventDateTime().also { - it.dateTime = DateTime(timeMillis - millisZoneOffset) - it.timeZone = BookingConstants.DEFAULT_TIMEZONE_ID - } + private fun Instant.toGoogleEventDateTime():EventDateTime { + val googleEventDateTime = EventDateTime() + googleEventDateTime.dateTime = this.toEpochMilli().toGoogleDateTime() + googleEventDateTime.timeZone = BookingConstants.DEFAULT_TIMEZONE_ID + return googleEventDateTime } private fun userModelToAttendee(model: UserModel): EventAttendee { diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt index 4f78d2e98..4a4f459d2 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt @@ -10,6 +10,7 @@ import office.effective.common.exception.MissingIdException import office.effective.common.exception.WorkspaceUnavailableException import office.effective.features.calendar.repository.CalendarIdsRepository import office.effective.features.booking.converters.GoogleCalendarConverter +import office.effective.features.booking.converters.toGoogleDateTime import office.effective.features.user.repository.UserRepository import office.effective.model.Booking import office.effective.features.user.repository.UserEntity @@ -114,9 +115,12 @@ class BookingMeetingRepository( * Request template containing all required parameters * * @param timeMin lover bound for filtering bookings by start time. - * Old Google calendar events may not appear correctly in the system and cause unexpected exceptions - * @param timeMax - * @param singleEvents + * Old Google calendar events may not appear correctly in the system and cause unexpected exceptions. + * Should be a time in the default timezone. + * @param timeMax upper bound (exclusive) for an event's start time to filter by. + * Should be a time in the default timezone. + * @param singleEvents Whether to expand recurring events into instances and only return single one-off + * events and instances of recurring events, but not the underlying recurring events themselves. * @param calendarId */ private fun basicQuery( @@ -127,8 +131,8 @@ class BookingMeetingRepository( ): Calendar.Events.List { return calendarEvents.list(calendarId) .setSingleEvents(singleEvents) - .setTimeMin(DateTime(timeMin)) - .setTimeMax(timeMax?.let { DateTime(it) }) + .setTimeMin(timeMin.toGoogleDateTime()) + .setTimeMax(timeMax?.toGoogleDateTime()) } /** @@ -483,12 +487,11 @@ class BookingMeetingRepository( * @return True if event has a collision * */ private fun singleEventHasCollision(eventToVerify: Event, workspaceCalendar: String): Boolean { - val sameTimeEvents = basicQuery( - timeMin = eventToVerify.start.dateTime.value, - timeMax = eventToVerify.end.dateTime.value, - singleEvents = true, - calendarId = workspaceCalendar - ).execute().items + val sameTimeEvents = calendarEvents.list(workspaceCalendar) + .setSingleEvents(true) + .setTimeMin(eventToVerify.start.dateTime) + .setTimeMax(eventToVerify.end.dateTime) + .execute().items for (event in sameTimeEvents) { if (areEventsOverlap(eventToVerify, event) && eventsIsNotSame(eventToVerify, event)) { @@ -538,9 +541,12 @@ class BookingMeetingRepository( * Checks whether events aren't the same event or instances of the same event */ private fun eventsIsNotSame(firstEvent: Event, secondEvent: Event): Boolean { - return firstEvent.id != secondEvent.id && - firstEvent.id != secondEvent.recurringEventId && - firstEvent.recurringEventId != secondEvent.id && - (firstEvent.recurringEventId != firstEvent.recurringEventId || firstEvent.recurringEventId == null) + var eventsDoNotBelongToSameRecurringEvent = true + if (firstEvent.recurringEventId != null || secondEvent.recurringEventId != null) { + eventsDoNotBelongToSameRecurringEvent = firstEvent.id != secondEvent.recurringEventId && + firstEvent.recurringEventId != secondEvent.id && + firstEvent.recurringEventId != firstEvent.recurringEventId + } + return firstEvent.id != secondEvent.id && eventsDoNotBelongToSameRecurringEvent } } \ No newline at end of file diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt index 204d62ceb..b1cc84af1 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt @@ -9,6 +9,7 @@ import office.effective.common.exception.InstanceNotFoundException import office.effective.common.exception.MissingIdException import office.effective.common.exception.WorkspaceUnavailableException import office.effective.features.booking.converters.GoogleCalendarConverter +import office.effective.features.booking.converters.toGoogleDateTime import office.effective.model.Booking import org.slf4j.LoggerFactory import java.time.Instant @@ -109,8 +110,8 @@ class BookingRegularRepository( ): Calendar.Events.List { return calendarEvents.list(calendarId) .setSingleEvents(singleEvents) - .setTimeMin(DateTime(timeMin)) - .setTimeMax(timeMax?.let { DateTime(it) }) + .setTimeMin(timeMin.toGoogleDateTime()) + .setTimeMax(timeMax?.toGoogleDateTime()) } /** @@ -369,12 +370,13 @@ class BookingRegularRepository( * @return True if event has a collision and can't be saved * */ private fun singleEventHasCollision(eventToVerify: Event, workspaceId: UUID): Boolean { - val sameTimeEvents = basicQuery( - timeMin = eventToVerify.start.dateTime.value, - timeMax = eventToVerify.end.dateTime.value, - singleEvents = true, - ).setQ(workspaceId.toString()) + val sameTimeEvents = calendarEvents.list(regularWorkspacesCalendar) + .setSingleEvents(true) + .setTimeMin(eventToVerify.start.dateTime) + .setTimeMax(eventToVerify.end.dateTime) + .setQ(workspaceId.toString()) .execute().items + for (event in sameTimeEvents) { if (areEventsOverlap(eventToVerify, event) && eventsIsNotSame(eventToVerify, event)) { return true @@ -424,9 +426,12 @@ class BookingRegularRepository( * Checks whether events aren't the same event or instances of the same event */ private fun eventsIsNotSame(firstEvent: Event, secondEvent: Event): Boolean { - return firstEvent.id != secondEvent.id && - firstEvent.id != secondEvent.recurringEventId && - firstEvent.recurringEventId != secondEvent.id && - (firstEvent.recurringEventId != firstEvent.recurringEventId || firstEvent.recurringEventId == null) + var eventsDoNotBelongToSameRecurringEvent = true + if (firstEvent.recurringEventId != null || secondEvent.recurringEventId != null) { + eventsDoNotBelongToSameRecurringEvent = firstEvent.id != secondEvent.recurringEventId && + firstEvent.recurringEventId != secondEvent.id && + firstEvent.recurringEventId != firstEvent.recurringEventId + } + return firstEvent.id != secondEvent.id && eventsDoNotBelongToSameRecurringEvent } } From 06e87236ea0fcd691a3268d8eadab1175f529d1a Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Mon, 22 Apr 2024 00:07:19 +0600 Subject: [PATCH 19/26] [~] fix EventDateTime conversion to Instant --- .../booking/converters/DateExtensions.kt | 2 +- .../converters/GoogleCalendarConverter.kt | 31 +++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/DateExtensions.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/DateExtensions.kt index 359b99c4b..a7c8f2b7d 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/DateExtensions.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/DateExtensions.kt @@ -10,4 +10,4 @@ import office.effective.common.constants.BookingConstants */ fun Long.toGoogleDateTime(): DateTime { return DateTime(this - BookingConstants.DEFAULT_TIMEZONE_OFFSET_MILLIS) -} \ No newline at end of file +} diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt index ae1855f1b..946a7759a 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt @@ -51,8 +51,8 @@ class GoogleCalendarConverter( participants = emptyList(), workspace = getWorkspaceModel(event), id = event.id ?: null, - beginBooking = Instant.ofEpochMilli(event.start?.dateTime?.value ?: 0), - endBooking = Instant.ofEpochMilli(event.end?.dateTime?.value ?: 1), + beginBooking = toLocalInstant(event.start), + endBooking = toLocalInstant(event.end), recurrence = recurrence?.let { RecurrenceConverter.recurrenceToModel(it) }, recurringBookingId = event.recurringEventId ) @@ -129,13 +129,13 @@ class GoogleCalendarConverter( * @author Danil Kiselev, Max Mishenko */ fun toMeetingWorkspaceBooking(event: Event): Booking { - logger.debug("[toGoogleEvent] converting calendar event to meeting room booking model") + logger.debug("[toMeetingWorkspaceBooking] converting calendar event to meeting room booking model") val organizer: String = event.organizer?.email ?: "" val email = if (organizer != defaultAccount) { - logger.trace("[toBookingModel] organizer email derived from event.organizer field") + logger.trace("[toMeetingWorkspaceBooking] organizer email derived from event.organizer field") organizer } else { - logger.trace("[toBookingModel] organizer email derived from event description") + logger.trace("[toMeetingWorkspaceBooking] organizer email derived from event description") event.description?.substringBefore(" ") ?: "" } val recurrence = event.recurrence?.toString()?.getRecurrence() @@ -145,11 +145,11 @@ class GoogleCalendarConverter( participants = getParticipantsModels(event), workspace = getWorkspaceModel(getCalendarId(event)), id = event.id ?: null, - beginBooking = Instant.ofEpochMilli(event.start?.dateTime?.value ?: 0), - endBooking = Instant.ofEpochMilli(event.end?.dateTime?.value ?: 1), + beginBooking = toLocalInstant(event.start), + endBooking = toLocalInstant(event.end), recurrence = recurrence?.let { RecurrenceConverter.recurrenceToModel(it) } ) - logger.trace("[toBookingModel] {}", booking.toString()) + logger.trace("[toMeetingWorkspaceBooking] {}", booking.toString()) return booking } @@ -163,7 +163,7 @@ class GoogleCalendarConverter( private fun getUserModel(email: String): UserModel { val userModel: UserModel = userRepository.findByEmail(email) ?: run { - logger.warn("[getUser] can't find a user with email ${email}. Creating placeholder.") + logger.warn("[getUserModel] can't find a user with email ${email}. Creating placeholder.") UserModel( id = null, fullName = "Unregistered user", @@ -313,12 +313,23 @@ class GoogleCalendarConverter( return attendees } + /** + * Converts [EventDateTime] to [Instant]. Returns placeholder if [googleDateTime] is null + * + * @return [Instant] + */ + private fun toLocalInstant(googleDateTime: EventDateTime?) : Instant { + val gmtEpoch: Long? = googleDateTime?.dateTime?.value + val localEpoch = gmtEpoch?.let { it + BookingConstants.DEFAULT_TIMEZONE_OFFSET_MILLIS } ?: 0 + return Instant.ofEpochMilli(localEpoch) + } + /** * Converts [Instant] to [EventDateTime] * * @return [EventDateTime] */ - private fun Instant.toGoogleEventDateTime():EventDateTime { + private fun Instant.toGoogleEventDateTime() : EventDateTime { val googleEventDateTime = EventDateTime() googleEventDateTime.dateTime = this.toEpochMilli().toGoogleDateTime() googleEventDateTime.timeZone = BookingConstants.DEFAULT_TIMEZONE_ID From e468fad9cb52e9d6c0ab6d39983c545e366f26e2 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Mon, 22 Apr 2024 00:56:28 +0600 Subject: [PATCH 20/26] [~] fix until --- .../kotlin/office/effective/model/RecurrenceModel.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/model/RecurrenceModel.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/model/RecurrenceModel.kt index a4744e148..cf2a8665a 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/model/RecurrenceModel.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/model/RecurrenceModel.kt @@ -1,11 +1,10 @@ package office.effective.model -import model.RecurrenceDTO import office.effective.common.constants.BookingConstants -import java.lang.IllegalArgumentException import java.text.SimpleDateFormat import java.util.* + data class RecurrenceModel ( val interval: Int? = null, val freq: String, @@ -37,10 +36,12 @@ data class RecurrenceModel ( * * @param millisDate - date in milliseconds ([Long]) * @return [String] - date in DATE-TIME (RFC5545). Example: [BookingConstants.UNTIL_FORMAT] - * @author Kiselev Danil * */ private fun toDateRfc5545(millisDate: Long): String { - val time = GregorianCalendar().apply { timeInMillis = millisDate + 86400000 } + val time = GregorianCalendar().apply { + timeZone = TimeZone.getTimeZone("GMT") + timeInMillis = millisDate + } return SimpleDateFormat(BookingConstants.UNTIL_FORMAT).format(time.time) } } From 40f015a662c0f08fbe2efd101cefde2377732946 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Mon, 22 Apr 2024 01:22:20 +0600 Subject: [PATCH 21/26] [~] fix routing --- .../features/booking/routes/BookingRoutingV1.kt | 9 +++++---- .../features/workspace/routes/WorkspaceRoutingV1.kt | 2 +- .../office/effective/plugins/RequestValidation.kt | 12 ++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt index bb3bd915b..6e3b5e646 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/BookingRoutingV1.kt @@ -22,6 +22,7 @@ import java.time.ZoneId fun Route.bookingRoutingV1() { route("/api/v1/bookings") { val bookingFacade: BookingFacadeV1 = GlobalContext.get().get() + val oneDayMillis = 1000*60*60*24 get("/{id}", SwaggerDocument.returnBookingByIdV1()) { val id: String = call.parameters["id"] @@ -33,8 +34,8 @@ fun Route.bookingRoutingV1() { val todayEpoch = LocalDate.now() .atStartOfDay(ZoneId.of(BookingConstants.DEFAULT_TIMEZONE_ID)) .toInstant() - .toEpochMilli() - val endOfDayEpoch = todayEpoch + 1000*60*60*24 + .toEpochMilli() + BookingConstants.DEFAULT_TIMEZONE_OFFSET_MILLIS + val endOfDayEpoch = todayEpoch + oneDayMillis val userId: String? = call.request.queryParameters["user_id"] val workspaceId: String? = call.request.queryParameters["workspace_id"] @@ -46,11 +47,11 @@ fun Route.bookingRoutingV1() { val bookingRangeTo: Long = call.request.queryParameters["range_to"]?.let { stringRangeTo -> stringRangeTo.toLongOrNull() ?: throw BadRequestException("range_to can't be parsed to Long") - } ?: todayEpoch + } ?: endOfDayEpoch val bookingRangeFrom: Long = call.request.queryParameters["range_from"]?.let { stringRangeFrom -> stringRangeFrom.toLongOrNull() ?: throw BadRequestException("range_from can't be parsed to Long") - } ?: endOfDayEpoch + } ?: todayEpoch call.respond(bookingFacade.findAll(userId, workspaceId, bookingRangeTo, bookingRangeFrom, returnInstances)) } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt index dc8894f18..79d6ae3f3 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt @@ -44,7 +44,7 @@ fun Route.workspaceRoutingV1() { val todayEpoch = LocalDate.now() .atStartOfDay(ZoneId.of(BookingConstants.DEFAULT_TIMEZONE_ID)) .toInstant() - .toEpochMilli() + .toEpochMilli() + BookingConstants.DEFAULT_TIMEZONE_OFFSET_MILLIS val endOfDayEpoch = todayEpoch + oneDayMillis withBookingsFrom = withBookingsFromString?.let { paramString -> paramString.toLongOrNull() diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/RequestValidation.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/RequestValidation.kt index 9d6c6d078..ca6f023ef 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/RequestValidation.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/RequestValidation.kt @@ -3,6 +3,7 @@ package office.effective.plugins import io.ktor.server.application.* import io.ktor.server.plugins.requestvalidation.* import office.effective.dto.BookingDTO +import office.effective.dto.BookingRequestDTO fun Application.configureValidation() { install(RequestValidation) { @@ -17,5 +18,16 @@ fun Application.configureValidation() { ) else ValidationResult.Valid } + validate { booking -> + if (booking.beginBooking < 0L || booking.beginBooking >= 2147483647000L) + ValidationResult.Invalid("beginBooking should be non-negative and less than timestamp max value") + else if (booking.endBooking < 0L || booking.endBooking >= 2147483647000L) + ValidationResult.Invalid("endBooking should be non-negative and less than timestamp max value") + else if (booking.endBooking <= booking.beginBooking) + ValidationResult.Invalid( + "endBooking (${booking.endBooking}) should be greater than beginBooking (${booking.beginBooking})" + ) + else ValidationResult.Valid + } } } From c40df525b0202aafc123ef022e4fcd09a8a76b7b Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Mon, 22 Apr 2024 01:36:44 +0600 Subject: [PATCH 22/26] [~] fix collision --- .../features/booking/repository/BookingMeetingRepository.kt | 2 +- .../features/booking/repository/BookingRegularRepository.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt index 4a4f459d2..12996e705 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt @@ -545,7 +545,7 @@ class BookingMeetingRepository( if (firstEvent.recurringEventId != null || secondEvent.recurringEventId != null) { eventsDoNotBelongToSameRecurringEvent = firstEvent.id != secondEvent.recurringEventId && firstEvent.recurringEventId != secondEvent.id && - firstEvent.recurringEventId != firstEvent.recurringEventId + firstEvent.recurringEventId != secondEvent.recurringEventId } return firstEvent.id != secondEvent.id && eventsDoNotBelongToSameRecurringEvent } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt index b1cc84af1..7819db5a2 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt @@ -430,7 +430,7 @@ class BookingRegularRepository( if (firstEvent.recurringEventId != null || secondEvent.recurringEventId != null) { eventsDoNotBelongToSameRecurringEvent = firstEvent.id != secondEvent.recurringEventId && firstEvent.recurringEventId != secondEvent.id && - firstEvent.recurringEventId != firstEvent.recurringEventId + firstEvent.recurringEventId != secondEvent.recurringEventId } return firstEvent.id != secondEvent.id && eventsDoNotBelongToSameRecurringEvent } From 6dfde4583c569ceb0f1efbc30ed0f1876e3fe262 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Mon, 22 Apr 2024 02:14:11 +0600 Subject: [PATCH 23/26] [~] converter optimisation --- .../converters/GoogleCalendarConverter.kt | 38 +++++++++++++++---- .../repository/BookingMeetingRepository.kt | 16 ++++++-- .../repository/BookingRegularRepository.kt | 16 ++++++-- .../effective/booking/BookingFacadeTest.kt | 2 +- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt index 946a7759a..5e061f1de 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt @@ -40,16 +40,27 @@ class GoogleCalendarConverter( * Creates placeholders if workspace or owner doesn't exist in database * * @param event [Event] to be converted + * @param owner specify this parameter to reduce the number + * of database queries if the owner has already been retrieved + * @param participants specify this parameter to reduce the number + * of database queries if participants have already been retrieved + * @param workspace specify this parameter to reduce the number + * of database queries if workspace have already been retrieved * @return The resulting [Booking] object */ - fun toRegularWorkspaceBooking(event: Event): Booking { + fun toRegularWorkspaceBooking( + event: Event, + owner: UserModel? = null, + workspace: Workspace? = null, + participants: List? = null, + ): Booking { logger.debug("[toRegularWorkspaceBooking] converting an event to workspace booking dto") val recurrence = event.recurrence?.toString()?.getRecurrence() val model = Booking( - owner = getUserModel(event), - participants = emptyList(), - workspace = getWorkspaceModel(event), + owner = owner ?: getUserModel(event), + participants = participants ?: emptyList(), + workspace = workspace ?: getWorkspaceModel(event), id = event.id ?: null, beginBooking = toLocalInstant(event.start), endBooking = toLocalInstant(event.end), @@ -125,10 +136,21 @@ class GoogleCalendarConverter( * Converts meeting [Event] to [Booking] * * @param event [Event] to be converted + * @param owner specify this parameter to reduce the number + * of database queries if the owner has already been retrieved + * @param participants specify this parameter to reduce the number + * of database queries if participants have already been retrieved + * @param workspace specify this parameter to reduce the number + * of database queries if workspace have already been retrieved * @return The resulting [BookingDTO] object * @author Danil Kiselev, Max Mishenko */ - fun toMeetingWorkspaceBooking(event: Event): Booking { + fun toMeetingWorkspaceBooking( + event: Event, + owner: UserModel? = null, + workspace: Workspace? = null, + participants: List? = null, + ): Booking { logger.debug("[toMeetingWorkspaceBooking] converting calendar event to meeting room booking model") val organizer: String = event.organizer?.email ?: "" val email = if (organizer != defaultAccount) { @@ -141,9 +163,9 @@ class GoogleCalendarConverter( val recurrence = event.recurrence?.toString()?.getRecurrence() val booking = Booking( - owner = getUserModel(email), - participants = getParticipantsModels(event), - workspace = getWorkspaceModel(getCalendarId(event)), + owner = owner ?: getUserModel(email), + participants = participants ?: getParticipantsModels(event), + workspace = workspace ?: getWorkspaceModel(getCalendarId(event)), id = event.id ?: null, beginBooking = toLocalInstant(event.start), endBooking = toLocalInstant(event.end), diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt index 12996e705..865518a8f 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingMeetingRepository.kt @@ -373,8 +373,12 @@ class BookingMeetingRepository( } else { saveSingleEvent(booking, workspaceCalendar) } - return googleCalendarConverter.toMeetingWorkspaceBooking(savedEvent) - .also { savedBooking -> + return googleCalendarConverter.toMeetingWorkspaceBooking( + event = savedEvent, + owner = booking.owner, + workspace = booking.workspace, + participants = booking.participants + ).also { savedBooking -> logger.trace("[save] saved booking: {}", savedBooking) } } @@ -429,8 +433,12 @@ class BookingMeetingRepository( } else { updateSingleEvent(booking, workspaceCalendar) } - return googleCalendarConverter.toMeetingWorkspaceBooking(updatedEvent) - .also { updatedBooking -> + return googleCalendarConverter.toMeetingWorkspaceBooking( + event = updatedEvent, + owner = booking.owner, + workspace = booking.workspace, + participants = booking.participants + ).also { updatedBooking -> logger.trace("[update] updated booking: {}", updatedBooking) } } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt index 7819db5a2..8d0d565e3 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/repository/BookingRegularRepository.kt @@ -257,8 +257,12 @@ class BookingRegularRepository( } else { saveSingleEvent(booking) } - return googleCalendarConverter.toRegularWorkspaceBooking(savedEvent) - .also { savedBooking -> + return googleCalendarConverter.toRegularWorkspaceBooking( + event = savedEvent, + owner = booking.owner, + workspace = booking.workspace, + participants = booking.participants + ).also { savedBooking -> logger.trace("[save] saved booking: {}", savedBooking) } } @@ -312,8 +316,12 @@ class BookingRegularRepository( } else { updateSingleEvent(booking) } - return googleCalendarConverter.toRegularWorkspaceBooking(updatedEvent) - .also { updatedBooking -> + return googleCalendarConverter.toRegularWorkspaceBooking( + event = updatedEvent, + owner = booking.owner, + workspace = booking.workspace, + participants = booking.participants + ).also { updatedBooking -> logger.trace("[update] updated booking: {}", updatedBooking) } } diff --git a/effectiveOfficeBackend/src/test/kotlin/office/effective/booking/BookingFacadeTest.kt b/effectiveOfficeBackend/src/test/kotlin/office/effective/booking/BookingFacadeTest.kt index 284e40c30..289ac18e1 100644 --- a/effectiveOfficeBackend/src/test/kotlin/office/effective/booking/BookingFacadeTest.kt +++ b/effectiveOfficeBackend/src/test/kotlin/office/effective/booking/BookingFacadeTest.kt @@ -119,7 +119,7 @@ class BookingFacadeTest { val expectedList = listOf(bookingMockDto, bookingMockDto) setUpMockTransactionManager() - whenever(service.findAll(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(existingList) + whenever(service.findAll(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(existingList) whenever(bookingDtoModelConverter.modelToDto(anyOrNull())).thenReturn(expectedList[0], expectedList[1]) val result = facade.findAll( From bba1c3127267ba60aebff500c2002ad6f0aec336 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sat, 27 Apr 2024 21:59:16 +0600 Subject: [PATCH 24/26] [~] fix until timezone --- .../office/effective/model/RecurrenceModel.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/model/RecurrenceModel.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/model/RecurrenceModel.kt index cf2a8665a..537e0fe9b 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/model/RecurrenceModel.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/model/RecurrenceModel.kt @@ -1,7 +1,12 @@ package office.effective.model +import io.ktor.util.date.* import office.effective.common.constants.BookingConstants import java.text.SimpleDateFormat +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter import java.util.* @@ -35,14 +40,13 @@ data class RecurrenceModel ( * Converts milliseconds date into exdate format from RFC5545 * * @param millisDate - date in milliseconds ([Long]) - * @return [String] - date in DATE-TIME (RFC5545). Example: [BookingConstants.UNTIL_FORMAT] + * @return [String] - date in DATE-TIME (RFC5545) * */ private fun toDateRfc5545(millisDate: Long): String { - val time = GregorianCalendar().apply { - timeZone = TimeZone.getTimeZone("GMT") - timeInMillis = millisDate - } - return SimpleDateFormat(BookingConstants.UNTIL_FORMAT).format(time.time) + val oneDayInMillis = 86400000L + val instant = Instant.ofEpochMilli(millisDate + oneDayInMillis) //convert inclusive bound to not inclusive one + val dateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC) + return dateTime.format(DateTimeFormatter.ofPattern(BookingConstants.UNTIL_FORMAT)) } } } \ No newline at end of file From 3317f6b42d6ffe143a8919b7324130f61f972e57 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sat, 27 Apr 2024 22:23:51 +0600 Subject: [PATCH 25/26] [-] delete timestamp max value --- .../common/constants/BookingConstants.kt | 1 - .../booking/facade/BookingFacadeV1.kt | 2 +- .../workspace/facade/WorkspaceFacadeV1.kt | 20 +++++++++---------- .../effective/plugins/RequestValidation.kt | 20 +++++++++---------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt index 2db157e36..a5d3e4c11 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/common/constants/BookingConstants.kt @@ -15,7 +15,6 @@ object BookingConstants { */ val MIN_SEARCH_START_TIME = config.propertyOrNull("calendar.minTime")?.getString()?.toLong() ?: throw Exception("Config file does not contain minimum time") - val MAX_TIMESTAMP = 2147483647000L val DEFAULT_CALENDAR: String = System.getenv("DEFAULT_CALENDAR") ?: config.propertyOrNull("calendar.defaultCalendar")?.getString() ?: throw Exception("Environment and config file does not contain Google default calendar id") diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt index 8123a23bd..7dbf8c668 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/facade/BookingFacadeV1.kt @@ -75,7 +75,7 @@ class BookingFacadeV1( returnInstances: Boolean ): List { if (bookingRangeTo != null && bookingRangeTo <= bookingRangeFrom) { - throw BadRequestException("Max booking start time should be null or greater than min start time") + throw BadRequestException("Max booking start time must be null or greater than min start time") } val bookingList: List = transactionManager.useTransaction({ bookingService.findAll( diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt index 07cffce7f..491bb8f28 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/facade/WorkspaceFacadeV1.kt @@ -63,13 +63,13 @@ class WorkspaceFacadeV1( withBookingsUntil: Long? = null ): List { if (withBookingsFrom != null && withBookingsUntil != null) { - if (withBookingsFrom < 0L || withBookingsFrom >= BookingConstants.MAX_TIMESTAMP) - throw ValidationException("with_bookings_from should be non-negative and greater than timestamp max value") - else if (withBookingsUntil < 0L || withBookingsUntil >= BookingConstants.MAX_TIMESTAMP) - throw ValidationException("with_bookings_until should be non-negative and less than timestamp max value") + if (withBookingsFrom < 0L) + throw ValidationException("with_bookings_from must be non-negative") + else if (withBookingsUntil < 0L) + throw ValidationException("with_bookings_until must be non-negative") else if (withBookingsUntil <= withBookingsFrom) throw ValidationException( - "with_bookings_until (${withBookingsUntil}) should be greater than with_bookings_from (${withBookingsFrom})") + "with_bookings_until (${withBookingsUntil}) must be greater than with_bookings_from (${withBookingsFrom})") return findAllByTag(tag).map { workspace -> val bookings = bookingFacade.findAll( @@ -112,13 +112,13 @@ class WorkspaceFacadeV1( * @author Daniil Zavyalov */ fun findAllFreeByPeriod(tag: String, beginTimestamp: Long, endTimestamp: Long): List { - if (beginTimestamp < 0L || beginTimestamp >= BookingConstants.MAX_TIMESTAMP) - throw ValidationException("Begin timestamp should be non-negative and less than timestamp max value") - else if (endTimestamp < 0L || endTimestamp >= BookingConstants.MAX_TIMESTAMP) - throw ValidationException("End timestamp should be non-negative and less than timestamp max value") + if (beginTimestamp < 0L) + throw ValidationException("Begin timestamp must be non-negative") + else if (endTimestamp < 0L) + throw ValidationException("End timestamp must be non-negative") else if (endTimestamp <= beginTimestamp) throw ValidationException( - "End timestamp (${endTimestamp}) should be greater than begin timestamp (${beginTimestamp})") + "End timestamp (${endTimestamp}) must be greater than begin timestamp (${beginTimestamp})") return transactionManager.useTransaction({ val modelList = service.findAllFreeByPeriod( diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/RequestValidation.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/RequestValidation.kt index ca6f023ef..885048381 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/RequestValidation.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/plugins/RequestValidation.kt @@ -8,24 +8,24 @@ import office.effective.dto.BookingRequestDTO fun Application.configureValidation() { install(RequestValidation) { validate { booking -> - if (booking.beginBooking < 0L || booking.beginBooking >= 2147483647000L) - ValidationResult.Invalid("beginBooking should be non-negative and less than timestamp max value") - else if (booking.endBooking < 0L || booking.endBooking >= 2147483647000L) - ValidationResult.Invalid("endBooking should be non-negative and less than timestamp max value") + if (booking.beginBooking < 0L) + ValidationResult.Invalid("beginBooking must be non-negative") + else if (booking.endBooking < 0L) + ValidationResult.Invalid("endBooking must be non-negative") else if (booking.endBooking <= booking.beginBooking) ValidationResult.Invalid( - "endBooking (${booking.endBooking}) should be greater than beginBooking (${booking.beginBooking})" + "endBooking (${booking.endBooking}) must be greater than beginBooking (${booking.beginBooking})" ) else ValidationResult.Valid } validate { booking -> - if (booking.beginBooking < 0L || booking.beginBooking >= 2147483647000L) - ValidationResult.Invalid("beginBooking should be non-negative and less than timestamp max value") - else if (booking.endBooking < 0L || booking.endBooking >= 2147483647000L) - ValidationResult.Invalid("endBooking should be non-negative and less than timestamp max value") + if (booking.beginBooking < 0L) + ValidationResult.Invalid("beginBooking must be non-negative") + else if (booking.endBooking < 0L) + ValidationResult.Invalid("endBooking must be non-negative") else if (booking.endBooking <= booking.beginBooking) ValidationResult.Invalid( - "endBooking (${booking.endBooking}) should be greater than beginBooking (${booking.beginBooking})" + "endBooking (${booking.endBooking}) must be greater than beginBooking (${booking.beginBooking})" ) else ValidationResult.Valid } From e0fe663f32280de4ec8dbcceb4797e68c3ff4840 Mon Sep 17 00:00:00 2001 From: Daniil Zavyalov Date: Sat, 27 Apr 2024 22:51:18 +0600 Subject: [PATCH 26/26] [~] refactor code --- .../converters/GoogleCalendarConverter.kt | 6 +- .../routes/swagger/BookingSwaggerV1.kt | 2 +- .../workspace/routes/WorkspaceRoutingV1.kt | 78 +++++++++++-------- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt index 5e061f1de..da465c605 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/converters/GoogleCalendarConverter.kt @@ -251,13 +251,11 @@ class GoogleCalendarConverter( */ fun toGoogleWorkspaceRegularEvent(model: Booking): Event { logger.debug("[toGoogleWorkspaceRegularEvent] converting regular workspace booking to calendar event") - val attendeeList: MutableList = participantsAndOwnerToAttendees(model) - val event = Event().apply { id = model.id summary = eventSummaryForRegularBooking(model) description = eventDescriptionRegularBooking(model) - attendees = attendeeList + attendees = listOf() recurrence = getRecurrenceFromRecurrenceModel(model) start = model.beginBooking.toGoogleEventDateTime() end = model.endBooking.toGoogleEventDateTime() @@ -287,7 +285,6 @@ class GoogleCalendarConverter( fun toGoogleWorkspaceMeetingEvent(model: Booking): Event { logger.debug("[toGoogleWorkspaceMeetingEvent] converting meeting room booking to calendar event") val attendeeList: MutableList = participantsAndOwnerToAttendees(model) - attendeeList.add(workspaceModelToAttendee(model.workspace)) val event = Event().apply { id = model.id @@ -332,6 +329,7 @@ class GoogleCalendarConverter( ownerAsAttendee.organizer = true attendees.add(ownerAsAttendee) } + attendees.add(workspaceModelToAttendee(model.workspace)) return attendees } diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt index 4c02a060b..2c9e43d19 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/booking/routes/swagger/BookingSwaggerV1.kt @@ -111,7 +111,7 @@ fun SwaggerDocument.returnBookingsV1(): OpenApiRoute.() -> Unit = { * @suppress */ fun SwaggerDocument.postBookingV1(): OpenApiRoute.() -> Unit = { - description = "Saves a given booking" + description = "Saves a given booking. Participants of regular workspace booking will be ignored." tags = listOf("Bookings V1") request { body { diff --git a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt index 79d6ae3f3..197da9e44 100644 --- a/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt +++ b/effectiveOfficeBackend/src/main/kotlin/office/effective/features/workspace/routes/WorkspaceRoutingV1.kt @@ -18,52 +18,68 @@ import java.time.ZoneId fun Route.workspaceRoutingV1() { route("/api/v1/workspaces") { val facade: WorkspaceFacadeV1 = GlobalContext.get().get() - val oneDayMillis: Long = 1000*60*60*24; + val oneDayMillis: Long = 1000*60*60*24 get("/{id}", SwaggerDocument.returnWorkspaceByIdV1()) { val id: String = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest) call.respond(facade.findById(id)) } + + suspend fun getWorkspacesRequest(call: ApplicationCall, tag: String) { + val withBookingsString: String? = call.request.queryParameters["with_bookings"] + val withBookingsFromString: String? = call.request.queryParameters["with_bookings_from"] + val withBookingsUntilString: String? = call.request.queryParameters["with_bookings_until"] + + var withBookingsFrom: Long? = null + var withBookingsUntil: Long? = null + val withBookings = withBookingsString?.let { paramString -> + paramString.toBooleanStrictOrNull() + ?: throw BadRequestException("with_bookings can't be parsed to boolean") + } ?: false + if (withBookings || withBookingsFromString != null || withBookingsUntilString != null) { + val todayEpoch = LocalDate.now() + .atStartOfDay(ZoneId.of(BookingConstants.DEFAULT_TIMEZONE_ID)) + .toInstant() + .toEpochMilli() + BookingConstants.DEFAULT_TIMEZONE_OFFSET_MILLIS + val endOfDayEpoch = todayEpoch + oneDayMillis + withBookingsFrom = withBookingsFromString?.let { paramString -> + paramString.toLongOrNull() + ?: throw BadRequestException("with_bookings_from can't be parsed to Long") + } ?: todayEpoch + withBookingsUntil = withBookingsUntilString?.let { paramString -> + paramString.toLongOrNull() + ?: throw BadRequestException("with_bookings_until can't be parsed to Long") + } ?: endOfDayEpoch + } + call.respond(facade.findAllByTag(tag, withBookingsFrom, withBookingsUntil)) + } + + suspend fun getFreeWorkspacesRequest( + call: ApplicationCall, + tag: String, + freeFromString: String?, + freeUntilString: String? + ) { + val freeFrom: Long = freeFromString?.toLongOrNull() + ?: throw BadRequestException("free_from not specified or invalid") + val freeUntil: Long = freeUntilString?.toLongOrNull() + ?: throw BadRequestException("free_until not specified or invalid") + call.respond(facade.findAllFreeByPeriod(tag, freeFrom, freeUntil)) + } + get(SwaggerDocument.returnWorkspaceByTagV1()) { val tag: String = call.request.queryParameters["workspace_tag"] ?: return@get call.respond(HttpStatusCode.BadRequest) val freeFromString: String? = call.request.queryParameters["free_from"] val freeUntilString: String? = call.request.queryParameters["free_until"] - val withBookingsString: String? = call.request.queryParameters["with_bookings"] - val withBookingsFromString: String? = call.request.queryParameters["with_bookings_from"] - val withBookingsUntilString: String? = call.request.queryParameters["with_bookings_until"] if (freeFromString == null && freeUntilString == null) { - var withBookingsFrom: Long? = null - var withBookingsUntil: Long? = null - val withBookings = withBookingsString?.let { paramString -> - paramString.toBooleanStrictOrNull() - ?: throw BadRequestException("with_bookings can't be parsed to boolean") - } ?: false - if (withBookings || withBookingsFromString != null || withBookingsUntilString != null) { - val todayEpoch = LocalDate.now() - .atStartOfDay(ZoneId.of(BookingConstants.DEFAULT_TIMEZONE_ID)) - .toInstant() - .toEpochMilli() + BookingConstants.DEFAULT_TIMEZONE_OFFSET_MILLIS - val endOfDayEpoch = todayEpoch + oneDayMillis - withBookingsFrom = withBookingsFromString?.let { paramString -> - paramString.toLongOrNull() - ?: throw BadRequestException("with_bookings_from can't be parsed to Long") - } ?: todayEpoch - withBookingsUntil = withBookingsUntilString?.let { paramString -> - paramString.toLongOrNull() - ?: throw BadRequestException("with_bookings_until can't be parsed to Long") - } ?: endOfDayEpoch - } - call.respond(facade.findAllByTag(tag, withBookingsFrom, withBookingsUntil)) + getWorkspacesRequest(call, tag) } else { - val freeFrom: Long = freeFromString?.toLongOrNull() - ?: throw BadRequestException("free_from not specified or invalid") - val freeUntil: Long = freeUntilString?.toLongOrNull() - ?: throw BadRequestException("free_until not specified or invalid") - call.respond(facade.findAllFreeByPeriod(tag, freeFrom, freeUntil)) + getFreeWorkspacesRequest(call, tag, freeFromString, freeUntilString) } } + get("/zones", SwaggerDocument.returnAllZonesV1()) { call.respond(facade.findAllZones()) }