Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.opencu.bookit.application.service.schedule;

import com.opencu.bookit.domain.model.schedule.WorkingDayConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;


@Service
public class ScheduleService {
LocalTime workingDayStart;
LocalTime workingDayEnd;
Long bookingSlotDuration;

public ScheduleService(@Value("${booking.start-work}") int workingDayStartHour,
@Value("${booking.end-work}") int workingDayEndHour,
@Value("${booking.booking-slot-duration}") Long bookingSlotDuration) {
this.workingDayStart = LocalTime.of(workingDayStartHour, 0);
this.workingDayEnd = LocalTime.of(workingDayEndHour, 0);
this.bookingSlotDuration = bookingSlotDuration;

}

public WorkingDayConfig getWorkingDayConfig(LocalDate date) {
if (date.getDayOfWeek() == DayOfWeek.SUNDAY) {
return new WorkingDayConfig(null, null, null, true);
}
return new WorkingDayConfig(workingDayStart, workingDayEnd, bookingSlotDuration, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void updateScheduleForNextDay() {

private void addDayIfWeekend(LocalDate date) {
if (loadNonWorkingDaySchedulePort.findById(date).isEmpty()) {
if (date.getDayOfWeek() == DayOfWeek.SATURDAY) {
if (date.getDayOfWeek() == DayOfWeek.SUNDAY) {
ScheduleModel schedule = new ScheduleModel();
schedule.setDay_off(date);
schedule.setTag(DayStatus.WEEKEND);
Expand Down
1 change: 1 addition & 0 deletions bootstrap/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ booking:
end-work: 21
hall-max-capacity: 4
zone-id: Europe/Moscow
booking-slot-duration: 3600000

messaging:
enabled: ${MESSAGING_ENABLED:false}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.opencu.bookit.domain.model.schedule;

import java.time.LocalDate;
import java.util.Optional;
import java.util.UUID;

/**
* Represents a domain concept of an exception to the regular weekly schedule.
* <p>
* This model is used for specific dates like public holidays or special events where
* the working hours differ from the default weekly template. An override for a given
* date always takes precedence over the regular {@link WeeklySchedule}.
* <p>
* The presence or absence of {@code workingHours} indicates whether the coworking
* is open or closed on this specific date.
*/
public record ScheduleOverride(
UUID id,
LocalDate overrideDate,
Optional<WorkingHours> workingHours,
Optional<String> description
) {
public boolean isDayOff() {
return workingHours.isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.opencu.bookit.domain.model.schedule;

import java.time.DayOfWeek;
import java.util.Optional;

/**
* Represents the domain concept of a regular weekly schedule.
* <p>
* This model defines the default working hours for a specific day of the week.
* A day can either be a working day, represented by present {@code workingHours},
* or a day off, represented by an empty optional.
*/
public record WeeklySchedule(
DayOfWeek dayOfWeek,
Optional<WorkingHours> workingHours
) {
public boolean isDayOff() {
return workingHours.isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.opencu.bookit.domain.model.schedule;

import java.time.LocalTime;

public record WorkingDayConfig (
LocalTime workingDayStart,
LocalTime workingDayEnd,
Long bookingSlotDuration,
Boolean isDayOff) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.opencu.bookit.domain.model.schedule;

import java.time.LocalTime;
import java.util.Objects;

/**
* A Value Object representing the opening and closing times for a single day.
* <p>
* This record enforces the invariant that the opening time must not be after the closing time,
* ensuring that only valid working hour ranges can be created within the domain.
*/
public record WorkingHours(LocalTime openingTime, LocalTime closingTime) {

/**
* Compact constructor to enforce business rules upon creation.
*
* @throws NullPointerException if openingTime or closingTime is null.
* @throws IllegalArgumentException if openingTime is after closingTime.
*/
public WorkingHours {
Objects.requireNonNull(openingTime, "Opening time must not be null");
Objects.requireNonNull(closingTime, "Closing time must not be null");
if (openingTime.isAfter(closingTime)) {
throw new IllegalArgumentException("Opening time must be before closing time.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.opencu.bookit.adapter.out.persistence.mapper.EventMapper;
import com.opencu.bookit.adapter.out.persistence.repository.EventRepository;
import com.opencu.bookit.adapter.out.persistence.repository.UserRepository;
import com.opencu.bookit.adapter.out.persistence.specifications.EventSpecifications;
import com.opencu.bookit.application.port.out.event.DeleteEventPort;
import com.opencu.bookit.application.port.out.event.LoadEventPort;
import com.opencu.bookit.application.port.out.event.SaveEventPort;
Expand Down Expand Up @@ -61,62 +62,15 @@ public Page<EventModel> findWithFilters(
Pageable pageable,
UUID currentUserId
) {
Specification<EventEntity> spec = Specification.where(null);

if (startDate != null && endDate != null) {
spec = spec.and((root, query, cb) ->
cb.between(root.get("startTime"),
LocalDateTime.of(startDate, LocalTime.of(0,0,0)),
LocalDateTime.of(endDate, LocalTime.of(0,0,0))
));
}
if (tags != null && !tags.isEmpty()) {
spec = spec.and((root, query, cb) ->
root.join("tags").in(tags));
}
if (formats != null && !formats.isEmpty()) {
spec = spec.and((root, query, cb) ->
root.join("formats").in(formats));
}
if (times != null && !times.isEmpty()) {
spec = spec.and((root, query, cb) ->
root.join("times").in(times));
}
if (participationFormats != null && !participationFormats.isEmpty()) {
spec = spec.and((root, query, cb) ->
root.join("participationFormats").in(participationFormats));
}

if (targetAudiences != null && !targetAudiences.isEmpty()) {
spec = spec.and((root, query, cb) ->
root.join("targetAudiences").in(targetAudiences));
}
if (search != null && !search.isBlank()) {
spec = spec.and((root, query, cb) ->
cb.or(
cb.like(cb.lower(root.get("name")), "%" + search.toLowerCase() + "%"),
cb.like(cb.lower(root.get("short_description")), "%" + search.toLowerCase() + "%"),
cb.like(cb.lower(root.get("full_description")), "%" + search.toLowerCase() + "%")
)
);
}

if (status != null && currentUserId != null) {
Optional<UserEntity> userOpt = userRepository.findById(currentUserId);
if (userOpt.isPresent()) {
UserEntity user = userOpt.get();
if (status.equalsIgnoreCase("registered")) {
spec = spec.and((root, query, cb) ->
cb.isMember(user, root.get("users")));
} else if (status.equalsIgnoreCase("available")) {
spec = spec.and((root, query, cb) -> cb.and(
cb.greaterThan(root.get("available_places"), 0),
cb.not(cb.isMember(user, root.get("users")))
));
}
}
}

Specification<EventEntity> spec = Specification
.where(EventSpecifications.startBetweenInclusive(startDate, endDate))
.and(EventSpecifications.hasAnyTags(tags))
.and(EventSpecifications.hasAnyFormats(formats))
.and(EventSpecifications.hasAnyTimes(times))
.and(EventSpecifications.hasAnyParticipationFormats(participationFormats))
.and(EventSpecifications.hasAnyTargetAudiences(targetAudiences))
.and(EventSpecifications.search(search))
.and(EventSpecifications.withStatus(status, currentUserId));
return eventRepository.findAll(spec, pageable).map(eventMapper::toModel);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.opencu.bookit.adapter.out.persistence.entity.NewsEntity;
import com.opencu.bookit.adapter.out.persistence.mapper.NewsMapper;
import com.opencu.bookit.adapter.out.persistence.repository.NewsRepository;
import com.opencu.bookit.adapter.out.persistence.specifications.NewsSpecifications;
import com.opencu.bookit.application.port.out.news.DeleteNewsPort;
import com.opencu.bookit.application.port.out.news.LoadNewsPort;
import com.opencu.bookit.application.port.out.news.SaveNewsPort;
Expand Down Expand Up @@ -38,25 +39,11 @@ public List<NewsModel> findAll() {
}
@Override
public Page<NewsModel> findWithFilters(Set<ThemeTags> tags, String search, Pageable pageable) {
var spec = buildSpecification(tags, search);
return newsRepository.findAll(spec, pageable).map(newsMapper::toModel);
}
Specification<NewsEntity> spec = Specification
.where(NewsSpecifications.hasAnyTags(tags))
.and(NewsSpecifications.search(search));

public Specification<NewsEntity> buildSpecification(Set<ThemeTags> tags, String search) {
Specification<NewsEntity> spec = Specification.where(null);
if (tags != null && !tags.isEmpty()) {
spec = spec.and((root, query, cb) -> root.join("tags").in(tags));
}
if (search != null && !search.isBlank()) {
spec = spec.and((root, query, cb) ->
cb.or(
cb.like(cb.lower(root.get("title")), "%" + search.toLowerCase() + "%"),
cb.like(root.get("short_description"), "%" + search + "%"),
cb.like(root.get("full_description"), "%" + search + "%")
)
);
}
return spec;
return newsRepository.findAll(spec, pageable).map(newsMapper::toModel);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

@Component
@RequiredArgsConstructor
public class NonWorkingDaySchedulePersistenceAdapter implements LoadNonWorkingDaySchedulePort, SaveSchedulePort {
public class ScheduleAdapter implements LoadNonWorkingDaySchedulePort, SaveSchedulePort {

private final ScheduleRepository scheduleRepository;
private final ScheduleMapper scheduleMapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class NewsEntity {
@Column(nullable = false)
private String title;

@Column(nullable = false, columnDefinition = "TEXT")
@Column(nullable = false)
private String full_description;

@Column
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.opencu.bookit.adapter.out.persistence.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.UUID;

/**
* Represents an exception or override to the regular weekly schedule for a specific date.
* <p>
* This entity is used for public holidays, special events, or any day when the
* working hours differ from the default {@link WeeklyScheduleEntity}.
* An entry in this table for a given date takes precedence over the weekly rule.
*/
@Entity
@Table(name = "SCHEDULE_OVERRIDES")
@Getter
@Setter
@NoArgsConstructor
@ToString
public class ScheduleOverrideEntity {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "id", updatable = false, nullable = false)
private UUID id;

@Column(name = "override_date", nullable = false, unique = true)
private LocalDate overrideDate;

@Column(name = "is_day_off", nullable = false)
private boolean isDayOff = false;

/**
* The special opening time for this date.
* Null if it's a day off.
*/
@Column(name = "opening_time")
private LocalTime openingTime;

/**
* The special closing time for this date.
* Null if it's a day off.
*/
@Column(name = "closing_time")
private LocalTime closingTime;

@Column(name = "description")
private String description;

/**
* Ensures uniform appearance of records in the database.
* If it's a day off, opening and closing times are set to null.
* If it's a working day, both times must be non-null.
*/
@PrePersist
@PreUpdate
private void validateTimes() {
if (isDayOff) {
openingTime = null;
closingTime = null;
} else {
if (openingTime == null || closingTime == null) {
throw new IllegalStateException("Opening and closing times must be set for a working day override.");
}
}
}
}
Loading