Коммит 21d60370 создал по автору Radch-enko's avatar Radch-enko
Просмотр файлов

Feature: unify ID handling and enhance booking logic

- Switched from UUID to String-based ID handling for booking entities and improved ID validation across services and DTOs.
- Enhanced booking updates by supporting owner and workspace updates via `UpdateBookingDto`.
- Integrated workspace bookings into `WorkspaceDTO` for API response enhancement.
- Updated Google and dummy calendar providers for consistent event ID usage and improved logging.
- Removed redundant println statement in Firebase configuration.
- Adjusted tests and mapping layers to align with the ID handling changes.
владелец b4088adb
......@@ -27,7 +27,7 @@ class DummyCalendarProvider : CalendarProvider {
val externalEventId = "dummy-event-${UUID.randomUUID()}"
// Store the booking with the external event ID
val bookingWithExternalId = booking.copy(externalEventId = externalEventId)
val bookingWithExternalId = booking.copy(id = externalEventId)
bookings[externalEventId] = bookingWithExternalId
logger.debug("Created dummy event with ID: {}", externalEventId)
......@@ -37,8 +37,7 @@ class DummyCalendarProvider : CalendarProvider {
override fun updateEvent(booking: Booking): Booking {
logger.debug("Updating dummy event for booking: {}", booking)
val externalEventId = booking.externalEventId
?: throw IllegalArgumentException("Booking must have an external event ID to be updated")
val externalEventId = booking.id
// Check if the booking exists
if (!bookings.containsKey(externalEventId)) {
......@@ -55,9 +54,7 @@ class DummyCalendarProvider : CalendarProvider {
override fun deleteEvent(booking: Booking) {
logger.debug("Deleting dummy event for booking: {}", booking)
val externalEventId = booking.externalEventId
?: throw IllegalArgumentException("Booking must have an external event ID to be deleted")
val externalEventId = booking.id
// Remove the booking
bookings.remove(externalEventId)
......@@ -98,7 +95,7 @@ class DummyCalendarProvider : CalendarProvider {
}
}
override fun findEventById(id: UUID): Booking? {
override fun findEventById(id: String): Booking? {
logger.debug("Finding dummy event with ID {}", id)
// In a real implementation, we would map the booking ID to the external event ID
......
......@@ -57,15 +57,13 @@ class GoogleCalendarProvider(
}.getOrNull()
if (savedEvent == null) throw NullPointerException("Failed to create event")
// Return the booking with the external event ID set
return booking.copy(externalEventId = savedEvent.id)
return booking.copy(id = savedEvent.id)
}
override fun updateEvent(booking: Booking): Booking {
logger.debug("Updating event for booking: {}", booking)
val externalEventId = booking.externalEventId
?: throw IllegalArgumentException("Booking must have an external event ID to be updated")
val eventId = booking.id
val workspaceCalendarId = getCalendarIdByWorkspace(booking.workspace.id)
......@@ -75,31 +73,28 @@ class GoogleCalendarProvider(
}
val event = convertToGoogleEvent(booking)
val updatedEvent = calendar.events().update(defaultCalendar, externalEventId, event).execute()
val updatedEvent = calendar.events().update(workspaceCalendarId, eventId, event).execute()
return booking.copy(externalEventId = updatedEvent.id)
return booking.copy(id = updatedEvent.id)
}
override fun deleteEvent(booking: Booking) {
logger.debug("Deleting event for booking: {}", booking)
booking.externalEventId
?: throw IllegalArgumentException("Booking must have an external event ID to be deleted")
deleteEventByBooking(booking)
val eventId = booking.id
deleteEventByBooking(booking, eventId)
}
private fun deleteEventByBooking(booking: Booking) {
private fun deleteEventByBooking(booking: Booking, eventId: String) {
try {
val calendarId = getCalendarIdByWorkspace(booking.workspace.id)
calendar.events().delete(calendarId, booking.externalEventId).execute()
calendar.events().delete(calendarId, eventId).execute()
} catch (e: GoogleJsonResponseException) {
logger.error("Failed to delete event: {}", e.details)
if (e.statusCode != 404 && e.statusCode != 410) {
throw e
}
// If the event doesn't exist (404) or has been deleted (410), ignore the exception
logger.warn("Event with ID {} not found or already deleted", booking.externalEventId)
logger.warn("Event with ID {} not found or already deleted", eventId)
}
}
......@@ -152,32 +147,22 @@ class GoogleCalendarProvider(
return bookings
}
override fun findEventById(id: UUID): Booking? {
override fun findEventById(id: String): Booking? {
logger.debug("Finding event with ID {}", id)
// Search for events with the booking ID in the description
// We need to search in all calendars because we don't know which calendar the event is in
// Get all calendar IDs
val calendarIds = workspaceDomainService.findAllCalendarIds().map { it.calendarId }
// Search for events with the booking ID in the description
// We'll search for events in the last year to limit the search
val oneYearAgo = Instant.now().minusSeconds(365 * 24 * 60 * 60) // TODO
for (calendarId in calendarIds) {
try {
// Search for events with the booking ID in the description
val events = listEvents(calendarId, oneYearAgo, null)
// Find the event with the exact booking ID
val event = events.firstOrNull { event ->
event.description?.contains(id.toString()) == true
}
// Try to get the event directly by ID
val event = calendar.events().get(calendarId, id).execute()
if (event != null) {
return convertToBooking(event, calendarId)
}
} catch (e: Exception) {
logger.warn("Failed to search for events in calendar {}: {}", calendarId, e.message)
// If the event is not found in this calendar, try the next one
logger.debug("Event with ID {} not found in calendar {}", id, calendarId)
}
}
......@@ -259,10 +244,9 @@ class GoogleCalendarProvider(
private fun convertToGoogleEvent(booking: Booking): Event {
val event = Event()
.setSummary("${booking.workspace.id} - workspace id (${booking.workspace.zone} - ${booking.workspace.name})")
.setSummary("Meet${booking.owner?.let { " ${it.firstName} ${it.lastName}" }.orEmpty() }")
.setDescription(
"\nBooking ID: ${booking.id}" +
"\n${booking.owner?.let { owner -> "Booking created by ${owner.id} - organizer id [ ${owner.email} ]" }}"
"${booking.owner?.email} - почта организатора"
)
.setStart(createEventDateTime(booking.beginBooking.toEpochMilli()))
.setEnd(createEventDateTime(booking.endBooking.toEpochMilli()))
......@@ -287,8 +271,19 @@ class GoogleCalendarProvider(
private fun convertToBooking(event: Event, calendarId: String? = null): Booking {
// Get the organizer's email and find the corresponding user
logger.debug("event.organizer?.email: ${event.organizer?.email}")
val organizerEmail = event.organizer.email ?: "unknown@example.com"
val owner = findOrCreateUserByEmail(organizerEmail)
val organizer = event.organizer.email
// Check if the user found by organizer email is a system user
val user = userDomainService.findByEmail(organizer)
val email = if (user != null && user.tag == "system") {
logger.trace("[toBookingDTO] organizer email derived from event description")
event.description?.substringBefore(" ") ?: ""
} else {
logger.trace("[toBookingDTO] organizer email derived from event.organizer field")
organizer
}
val owner = findOrCreateUserByEmail(email)
// Get the attendees' emails and find the corresponding users
val participants = event.attendees?.mapNotNull { attendee ->
......@@ -304,24 +299,22 @@ class GoogleCalendarProvider(
throw IllegalStateException("Workspace not found for calendar ID: $calendarId")
}
// Extract booking ID from event description or use a random UUID if not found
val bookingIdStr = event.description?.let {
val regex = "Booking ID: ([0-9a-f-]+)".toRegex()
// Extract recurring booking ID from event description if it exists
val recurringBookingIdStr = event.description?.let {
val regex = "Recurring Booking ID: ([0-9a-f-]+)".toRegex()
val matchResult = regex.find(it)
matchResult?.groupValues?.get(1)
}
val bookingId = bookingIdStr?.let { UUID.fromString(it) } ?: UUID.randomUUID()
return Booking(
id = bookingId,
id = event.id,
owner = owner,
participants = participants,
workspace = workspace,
beginBooking = Instant.ofEpochMilli(event.start.dateTime.value),
endBooking = Instant.ofEpochMilli(event.end.dateTime.value),
recurrence = RecurrenceRuleConverter.fromGoogleRecurrenceRule(event.recurrence),
externalEventId = event.id
recurringBookingId = recurringBookingIdStr
)
}
......@@ -375,6 +368,7 @@ class GoogleCalendarProvider(
if (events.isEmpty()) return true
return events.none { existingEvent ->
if (booking.id == existingEvent.id) return@none false
val existingStart = Instant.ofEpochMilli(existingEvent.start.dateTime.value)
val existingEnd = Instant.ofEpochMilli(existingEvent.end.dateTime.value)
val isOverlapping = startTime < existingEnd && existingStart < endTime
......
......@@ -71,13 +71,7 @@ class BookingController(
@Parameter(description = "Booking ID", required = true)
@PathVariable id: String
): ResponseEntity<BookingDto> {
val bookingId = try {
UUID.fromString(id)
} catch (e: IllegalArgumentException) {
throw BookingNotFoundException("Invalid booking ID format: $id , error:${e.message}")
}
val booking = bookingService.getBookingById(bookingId)
val booking = bookingService.getBookingById(id)
?: throw BookingNotFoundException("Booking with ID $id not found")
return ResponseEntity.ok(BookingDto.fromDomain(booking))
......@@ -266,10 +260,7 @@ class BookingController(
@Parameter(description = "Updated booking data", required = true)
@Valid @RequestBody updateBookingDto: UpdateBookingDto
): ResponseEntity<BookingDto> {
val bookingId = runCatching { UUID.fromString(id) }.getOrNull()
?: throw BookingNotFoundException("Invalid booking ID format: $id")
val existingBooking = bookingService.getBookingById(bookingId)
val existingBooking = bookingService.getBookingById(id)
?: throw BookingNotFoundException("Booking with ID $id not found")
// Get the participants by email
......@@ -282,8 +273,25 @@ class BookingController(
}
}
// Get the owner user by email if provided, otherwise create a system user
val owner = updateBookingDto.ownerEmail?.let {
userService.findByEmail(updateBookingDto.ownerEmail)
?: throw UserNotFoundException("Owner with email ${updateBookingDto.ownerEmail} not found")
}
// Convert workspaceId from String to UUID
val workspaceUuid = try {
UUID.fromString(updateBookingDto.workspaceId)
} catch (e: IllegalArgumentException) {
throw WorkspaceNotFoundException("Invalid workspace ID format: ${updateBookingDto.workspaceId}")
}
val workspace = workspaceService.findById(workspaceUuid)
?: throw WorkspaceNotFoundException("Workspace with ID ${updateBookingDto.workspaceId} not found")
// Convert DTO to a domain model and update the booking
val booking = updateBookingDto.toDomain(existingBooking, participants)
val booking = updateBookingDto.toDomain(existingBooking, participants, owner, workspace)
val updatedBooking = bookingService.updateBooking(booking)
return ResponseEntity.ok(BookingDto.fromDomain(updatedBooking))
......@@ -312,10 +320,7 @@ class BookingController(
@Parameter(description = "Booking ID", required = true)
@PathVariable id: String
): ResponseEntity<Void> {
val bookingId = runCatching { UUID.fromString(id) }.getOrNull()
?: throw BookingNotFoundException("Invalid booking ID format: $id")
val booking = bookingService.getBookingById(bookingId)
val booking = bookingService.getBookingById(id)
?: throw BookingNotFoundException("Booking with ID $id not found")
bookingService.deleteBooking(booking)
......
......@@ -57,10 +57,10 @@ interface CalendarProvider {
/**
* Finds an event by its ID.
*
* @param id The ID of the booking
* @param id The ID of the booking (Google event ID)
* @return The booking if found, null otherwise
*/
fun findEventById(id: UUID): Booking?
fun findEventById(id: String): Booking?
/**
* Finds all events across all workspaces within a time range.
......
......@@ -9,20 +9,19 @@ import java.util.UUID
* Represents a booking of a workspace.
*
* A booking is created by a user (owner) for a specific workspace and time period.
* It can optionally have participants, a recurrence pattern, and an external event ID
* It can optionally have participants, a recurrence pattern, and an ID
* from a calendar provider.
*
* For recurring bookings, the recurringBookingId field links individual booking instances
* to their parent recurring booking.
*/
data class Booking(
val id: UUID = UUID.randomUUID(),
val id: String = "",
val owner: User?,
val participants: List<User> = emptyList(),
val workspace: Workspace,
val beginBooking: Instant,
val endBooking: Instant,
val recurrence: RecurrenceModel? = null,
val externalEventId: String? = null, // ID returned by the calendar provider
val recurringBookingId: UUID? = null // ID of the recurring booking this booking belongs to
val recurringBookingId: String? = null // ID of the recurring booking this booking belongs to
)
......@@ -52,11 +52,11 @@ data class BookingDto(
owner = booking.owner?.let { UserDto.fromDomain(it) },
participants = booking.participants.map { UserDto.fromDomain(it) },
workspace = WorkspaceDto.fromDomain(booking.workspace),
id = booking.id.toString(),
id = booking.id,
beginBooking = booking.beginBooking.toEpochMilli(),
endBooking = booking.endBooking.toEpochMilli(),
recurrence = booking.recurrence?.let { RecurrenceDto.fromDomain(it) },
recurringBookingId = booking.recurringBookingId?.toString()
recurringBookingId = booking.recurringBookingId
)
}
}
......
......@@ -61,6 +61,13 @@ data class CreateBookingDto(
*/
@Schema(description = "Data for updating an existing booking")
data class UpdateBookingDto(
@Schema(description = "Email of the user updating the booking", example = "john.doe@example.com")
val ownerEmail: String?,
@field:NotNull(message = "Workspace ID is required")
@Schema(description = "ID of the workspace being booked", example = "123e4567-e89b-12d3-a456-426614174000")
val workspaceId: String,
@Schema(description = "Emails of users participating in the booking", example = "[\"jane.doe@example.com\"]")
val participantEmails: List<String> = emptyList(),
......@@ -81,10 +88,14 @@ data class UpdateBookingDto(
*/
fun toDomain(
existingBooking: Booking,
participants: List<User>
participants: List<User>,
owner: User?,
workspace: Workspace,
): Booking {
return existingBooking.copy(
owner = owner,
participants = participants,
workspace = workspace,
beginBooking = beginBooking?.let { Instant.ofEpochMilli(it) } ?: existingBooking.beginBooking,
endBooking = endBooking?.let { Instant.ofEpochMilli(it) } ?: existingBooking.endBooking,
recurrence = recurrence?.let { RecurrenceDto.toDomain(it) } ?: existingBooking.recurrence
......
......@@ -47,10 +47,10 @@ class BookingService(
/**
* Deletes a booking by its ID.
*
* @param id The ID of the booking to delete
* @param id The ID of the booking to delete (Google event ID)
* @return true if the booking was deleted, false if it wasn't found
*/
fun deleteBookingById(id: UUID): Boolean {
fun deleteBookingById(id: String): Boolean {
val booking = getBookingById(id) ?: return false
calendarProvider.deleteEvent(booking)
return true
......@@ -59,10 +59,10 @@ class BookingService(
/**
* Gets a booking by its ID.
*
* @param id The ID of the booking
* @param id The ID of the booking (Google event ID)
* @return The booking if found, null otherwise
*/
fun getBookingById(id: UUID): Booking? {
fun getBookingById(id: String): Booking? {
return calendarProvider.findEventById(id)
}
......
......@@ -21,7 +21,6 @@ class FirebaseConfig(
@Bean
fun objectMapper(): ObjectMapper {
println("[firebaseCredentials] finded: $firebaseCredentials")
return ObjectMapper()
.registerModule(com.fasterxml.jackson.module.kotlin.KotlinModule.Builder().build())
.registerModule(com.fasterxml.jackson.datatype.jsr310.JavaTimeModule())
......
......@@ -7,4 +7,6 @@ dependencies {
implementation(libs.jakarta.servlet.api)
implementation(libs.springdoc.openapi.starter.webmvc.ui)
}
\ Нет новой строки в конце файла
implementation(project(":backend:feature:booking:core"))
}
......@@ -2,6 +2,8 @@ package band.effective.office.backend.feature.workspace.core.controller
import band.effective.office.backend.core.domain.model.Workspace
import band.effective.office.backend.core.domain.model.WorkspaceZone
import band.effective.office.backend.feature.booking.core.dto.BookingDto
import band.effective.office.backend.feature.booking.core.service.BookingService
import band.effective.office.backend.feature.workspace.core.dto.WorkspaceDTO
import band.effective.office.backend.feature.workspace.core.dto.WorkspaceZoneDTO
import band.effective.office.backend.feature.workspace.core.service.WorkspaceService
......@@ -27,6 +29,7 @@ import org.springframework.web.bind.annotation.RestController
@Tag(name = "Workspaces", description = "API for managing workspaces")
class WorkspaceController(
private val workspaceService: WorkspaceService,
private val bookingService: BookingService,
) {
/**
......@@ -110,6 +113,15 @@ class WorkspaceController(
* Convert a domain Workspace to a WorkspaceDTO.
*/
private fun convertToDto(workspace: Workspace): WorkspaceDTO {
// Get bookings for the workspace for the next 1 day
val now = Instant.now()
val oneDayLater = now.plusSeconds(24 * 60 * 60) // 1 day in seconds
val bookings = bookingService.getBookingsByWorkspace(
workspaceId = workspace.id,
from = now,
to = oneDayLater
)
return WorkspaceDTO(
id = workspace.id.toString(),
name = workspace.name,
......@@ -127,7 +139,8 @@ class WorkspaceController(
name = zone.name
)
},
tag = workspace.tag
tag = workspace.tag,
bookings = bookings.map { BookingDto.fromDomain(it) }
)
}
......@@ -140,4 +153,4 @@ class WorkspaceController(
name = zone.name
)
}
}
\ Нет новой строки в конце файла
}
package band.effective.office.backend.feature.workspace.core.dto
import band.effective.office.backend.feature.booking.core.dto.BookingDto
import io.swagger.v3.oas.annotations.media.Schema
/**
......@@ -9,16 +10,19 @@ import io.swagger.v3.oas.annotations.media.Schema
data class WorkspaceDTO(
@Schema(description = "Unique identifier of the workspace", example = "550e8400-e29b-41d4-a716-446655440000")
val id: String,
@Schema(description = "Name of the workspace", example = "Meeting Room 1")
val name: String,
@Schema(description = "List of utilities available in the workspace")
val utilities: List<UtilityDTO>,
@Schema(description = "Zone where the workspace is located")
val zone: WorkspaceZoneDTO? = null,
@Schema(description = "Tag for categorizing the workspace", example = "meeting")
val tag: String
)
\ Нет новой строки в конце файла
val tag: String,
@Schema(description = "List of bookings for this workspace")
val bookings: List<BookingDto>? = null
)
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать