diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 7ebab26..2f812df 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1 +1 @@
-When performing a code review, respond in Russian.
+When performing a code review, respond in Russian!
diff --git a/.gitignore b/.gitignore
index 64703cb..744eca1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@ target/
*.iws
*.iml
*.ipr
+/.run/
diff --git a/.run/Deployment compose-local.run.xml b/.run/Deployment compose-local.run.xml
index 7822156..4f2962b 100644
--- a/.run/Deployment compose-local.run.xml
+++ b/.run/Deployment compose-local.run.xml
@@ -1,11 +1,11 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/MainApplication (local).run.xml b/.run/MainApplication (local).run.xml
index 36efc67..bd3928c 100644
--- a/.run/MainApplication (local).run.xml
+++ b/.run/MainApplication (local).run.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/.run/MainApplication.run.xml b/.run/MainApplication.run.xml
new file mode 100644
index 0000000..d0e71e8
--- /dev/null
+++ b/.run/MainApplication.run.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/StatApplication (local).run.xml b/.run/StatApplication (local).run.xml
index 25a3c74..ed683b0 100644
--- a/.run/StatApplication (local).run.xml
+++ b/.run/StatApplication (local).run.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/.run/StatApplication.run.xml b/.run/StatApplication.run.xml
index 800e562..65d72ff 100644
--- a/.run/StatApplication.run.xml
+++ b/.run/StatApplication.run.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/main-service/pom.xml b/main-service/pom.xml
index 7335124..05cdb5c 100644
--- a/main-service/pom.xml
+++ b/main-service/pom.xml
@@ -15,50 +15,48 @@
Main Service
+
+ ru.practicum
+ stats-client
+ 0.0.1-SNAPSHOT
+ compile
+
org.springframework.boot
spring-boot-starter-web
-
org.springframework.boot
spring-boot-starter-actuator
-
org.springframework.boot
spring-boot-starter-validation
-
org.postgresql
postgresql
runtime
-
-
- ru.practicum
- stats-client
- 0.0.1-SNAPSHOT
- compile
-
-
com.h2database
h2
runtime
-
org.projectlombok
lombok
provided
-
org.springframework.boot
spring-boot-starter-data-jpa
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
@@ -67,6 +65,10 @@
org.springframework.boot
spring-boot-maven-plugin
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
diff --git a/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java b/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java
new file mode 100644
index 0000000..7d521b8
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java
@@ -0,0 +1,4 @@
+package ru.practicum.category.controller;
+
+public class AdminCategoryController {
+}
diff --git a/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java b/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java
new file mode 100644
index 0000000..9c051bd
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java
@@ -0,0 +1,4 @@
+package ru.practicum.category.controller;
+
+public class PublicCategoryController {
+}
diff --git a/main-service/src/main/java/ru/practicum/category/dto/CategoryDto.java b/main-service/src/main/java/ru/practicum/category/dto/CategoryDto.java
index 04587bd..d5e735b 100644
--- a/main-service/src/main/java/ru/practicum/category/dto/CategoryDto.java
+++ b/main-service/src/main/java/ru/practicum/category/dto/CategoryDto.java
@@ -1,7 +1,13 @@
package ru.practicum.category.dto;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
public record CategoryDto(
Long id,
+
+ @NotBlank(message = "Название категории не может быть пустым")
+ @Size(min = 1, max = 50, message = "Название категории должно содержать от {min} до {max} символов")
String name
) {
}
diff --git a/main-service/src/main/java/ru/practicum/category/dto/NewCategoryDto.java b/main-service/src/main/java/ru/practicum/category/dto/NewCategoryDto.java
index c235f69..a57e7e5 100644
--- a/main-service/src/main/java/ru/practicum/category/dto/NewCategoryDto.java
+++ b/main-service/src/main/java/ru/practicum/category/dto/NewCategoryDto.java
@@ -4,8 +4,8 @@
import jakarta.validation.constraints.Size;
public record NewCategoryDto(
- @NotBlank(message = "Название категории не может быть пустой")
- @Size(min = 1, max = 50, message = "Название категории должно содержать от 3 до 50 символов")
+ @NotBlank(message = "Название категории не может быть пустым")
+ @Size(min = 1, max = 50, message = "Название категории должно содержать от {min} до {max} символов")
String name
) {
}
diff --git a/main-service/src/main/java/ru/practicum/category/mapper/CategoryMapper.java b/main-service/src/main/java/ru/practicum/category/mapper/CategoryMapper.java
new file mode 100644
index 0000000..417b392
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/mapper/CategoryMapper.java
@@ -0,0 +1,16 @@
+package ru.practicum.category.mapper;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+import ru.practicum.category.model.Category;
+
+@Mapper(componentModel = "spring")
+public interface CategoryMapper {
+
+ @Mapping(target = "id", ignore = true)
+ Category toEntity(NewCategoryDto dto);
+
+ CategoryDto toDto(Category category);
+}
diff --git a/main-service/src/main/java/ru/practicum/category/model/Category.java b/main-service/src/main/java/ru/practicum/category/model/Category.java
new file mode 100644
index 0000000..144c242
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/model/Category.java
@@ -0,0 +1,22 @@
+package ru.practicum.category.model;
+
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+
+@Entity
+@Table(name = "categories")
+@Getter
+@Setter(AccessLevel.PROTECTED)
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class Category {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
+ private Long id;
+
+ @Column(name = "name", unique = true, nullable = false, length = 50)
+ private String name;
+}
diff --git a/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java b/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java
new file mode 100644
index 0000000..6f4235f
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java
@@ -0,0 +1,4 @@
+package ru.practicum.category.repository;
+
+public class CategoryRepository {
+}
diff --git a/main-service/src/main/java/ru/practicum/category/service/CategoryService.java b/main-service/src/main/java/ru/practicum/category/service/CategoryService.java
new file mode 100644
index 0000000..e2dedef
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/service/CategoryService.java
@@ -0,0 +1,4 @@
+package ru.practicum.category.service;
+
+public interface CategoryService {
+}
diff --git a/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java b/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java
new file mode 100644
index 0000000..c8db8b8
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java
@@ -0,0 +1,4 @@
+package ru.practicum.category.service;
+
+public class CategoryServiceImpl implements CategoryService {
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/controller/AdminCompilationController.java b/main-service/src/main/java/ru/practicum/compilation/controller/AdminCompilationController.java
new file mode 100644
index 0000000..1897cf1
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/controller/AdminCompilationController.java
@@ -0,0 +1,4 @@
+package ru.practicum.compilation.controller;
+
+public class AdminCompilationController {
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/controller/PublicCompilationController.java b/main-service/src/main/java/ru/practicum/compilation/controller/PublicCompilationController.java
new file mode 100644
index 0000000..afe13ce
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/controller/PublicCompilationController.java
@@ -0,0 +1,4 @@
+package ru.practicum.compilation.controller;
+
+public class PublicCompilationController {
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java b/main-service/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java
index 8c6cc3f..9348e54 100644
--- a/main-service/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java
+++ b/main-service/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java
@@ -11,7 +11,7 @@ public record NewCompilationDto(
boolean pinned,
@NotBlank(message = "Заголовок подборки не может быть пустым")
- @Size(min = 1, max = 50, message = "Заголовок должен содержать от 1 до 50 символов")
+ @Size(min = 1, max = 50, message = "Заголовок должен содержать от {min} до {max} символов")
String title
) {
}
diff --git a/main-service/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java b/main-service/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java
index 73337d8..7d5063c 100644
--- a/main-service/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java
+++ b/main-service/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java
@@ -1,5 +1,6 @@
package ru.practicum.compilation.dto;
+import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.util.Set;
@@ -9,7 +10,8 @@ public record UpdateCompilationRequest(
boolean pinned,
- @Size(min = 1, max = 50, message = "Заголовок должен содержать от 1 до 50 символов")
+ @NotBlank(message = "Заголовок подборки не может быть пустым")
+ @Size(min = 1, max = 50, message = "Заголовок должен содержать от {min} до {max} символов")
String title
) {
}
diff --git a/main-service/src/main/java/ru/practicum/compilation/mapper/CompilationMapper.java b/main-service/src/main/java/ru/practicum/compilation/mapper/CompilationMapper.java
new file mode 100644
index 0000000..0f0c19f
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/mapper/CompilationMapper.java
@@ -0,0 +1,4 @@
+package ru.practicum.compilation.mapper;
+
+public class CompilationMapper {
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/model/Compilation.java b/main-service/src/main/java/ru/practicum/compilation/model/Compilation.java
new file mode 100644
index 0000000..f6986cc
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/model/Compilation.java
@@ -0,0 +1,36 @@
+package ru.practicum.compilation.model;
+
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import ru.practicum.event.model.Event;
+
+import java.util.Set;
+
+@Entity
+@Table(name = "compilations")
+@Getter
+@Setter(AccessLevel.PROTECTED)
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class Compilation {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
+ private Long id;
+
+ @Column(nullable = false, length = 50)
+ private String title;
+
+ @Column(nullable = false)
+ private boolean pinned = false;
+
+ @ManyToMany(fetch = FetchType.LAZY)
+ @JoinTable(
+ name = "compilation_events",
+ joinColumns = @JoinColumn(name = "compilation_id"),
+ inverseJoinColumns = @JoinColumn(name = "event_id")
+ )
+ private Set events;
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java b/main-service/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java
new file mode 100644
index 0000000..fde531f
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java
@@ -0,0 +1,7 @@
+package ru.practicum.compilation.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.practicum.compilation.model.Compilation;
+
+public interface CompilationRepository extends JpaRepository {
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/service/CompilationService.java b/main-service/src/main/java/ru/practicum/compilation/service/CompilationService.java
new file mode 100644
index 0000000..8d22d2a
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/service/CompilationService.java
@@ -0,0 +1,4 @@
+package ru.practicum.compilation.service;
+
+public interface CompilationService {
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java b/main-service/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java
new file mode 100644
index 0000000..663fca0
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java
@@ -0,0 +1,4 @@
+package ru.practicum.compilation.service;
+
+public class CompilationServiceImpl implements CompilationService {
+}
diff --git a/main-service/src/main/java/ru/practicum/event/controller/AdminEventController.java b/main-service/src/main/java/ru/practicum/event/controller/AdminEventController.java
new file mode 100644
index 0000000..1570a6e
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/controller/AdminEventController.java
@@ -0,0 +1,39 @@
+package ru.practicum.event.controller;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.SearchEventAdminRequest;
+import ru.practicum.event.dto.UpdateEventAdminRequest;
+import ru.practicum.event.service.EventService;
+
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/admin/events")
+public class AdminEventController {
+ private final EventService eventService;
+
+ @GetMapping
+ public List getEventsAdmin(@ModelAttribute @Valid SearchEventAdminRequest request) {
+ int size = (request.size() != null && request.size() > 0) ? request.size() : 10;
+ int from = request.from() != null ? request.from() : 0;
+ PageRequest pageRequest = PageRequest.of(from / size, size);
+ log.debug("Controller: getEventAdmin with filters {}", request);
+ return eventService.getEventsAdmin(request, pageRequest);
+ }
+
+ @PatchMapping("/{eventId}")
+ public EventFullDto updateEventAdmin(
+ @PathVariable Long eventId,
+ @RequestBody @Valid UpdateEventAdminRequest request
+ ) {
+ log.debug("Controller: updateEventAdmin with id={} with data {}", eventId, request);
+ return eventService.updateEventAdmin(eventId, request);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/controller/PrivateEventController.java b/main-service/src/main/java/ru/practicum/event/controller/PrivateEventController.java
new file mode 100644
index 0000000..b136f62
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/controller/PrivateEventController.java
@@ -0,0 +1,90 @@
+package ru.practicum.event.controller;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.EventShortDto;
+import ru.practicum.event.dto.NewEventDto;
+import ru.practicum.event.dto.UpdateEventUserRequest;
+import ru.practicum.event.service.EventService;
+import ru.practicum.request.dto.EventRequestStatusUpdateRequest;
+import ru.practicum.request.dto.EventRequestStatusUpdateResult;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.service.RequestService;
+
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/users")
+@RequiredArgsConstructor
+public class PrivateEventController {
+ private final EventService eventService;
+ private final RequestService requestService;
+
+ @GetMapping("/{userId}/events")
+ public List getEvents(
+ @PathVariable("userId") Long userId,
+ @RequestParam(defaultValue = "0") Integer from,
+ @RequestParam(defaultValue = "10") Integer size
+ ) {
+ Pageable pageable = PageRequest.of(from / size, size);
+ log.debug("Controller: getEvents with id={} with pageable {}", userId, pageable);
+ return eventService.getEvents(userId, pageable);
+ }
+
+ @PostMapping("/{userId}/events")
+ @ResponseStatus(HttpStatus.CREATED)
+ public EventFullDto createEvent(
+ @PathVariable("userId") Long userId,
+ @RequestBody @Valid NewEventDto newEventDto
+ ) {
+ log.debug("Controller: createEvent with id={} with data {}", userId, newEventDto);
+ return eventService.createEvent(userId, newEventDto);
+ }
+
+ @GetMapping("/{userId}/events/{eventId}")
+ public EventFullDto getEvent(
+ @PathVariable("userId") Long userId,
+ @PathVariable("eventId") Long eventId,
+ HttpServletRequest request
+ ) {
+ log.debug("Controller: getEvent with id={} and eventId={}", userId, eventId);
+ return eventService.getEvent(userId, eventId, request.getRemoteAddr());
+ }
+
+ @PatchMapping("/{userId}/events/{eventId}")
+ public EventFullDto updateEvent(
+ @PathVariable("userId") Long userId,
+ @PathVariable("eventId") Long eventId,
+ @RequestBody @Valid UpdateEventUserRequest request
+ ) {
+ log.debug("Controller: updateEvent with id={} and eventId={} with data {}", userId, eventId, request);
+ return eventService.updateEvent(userId, eventId, request);
+ }
+
+ @GetMapping("/{userId}/events/{eventId}/requests")
+ public List getRequestsByEvent(
+ @PathVariable("userId") Long userId,
+ @PathVariable("eventId") Long eventId
+ ) {
+ log.debug("Controller: getRequestsByEvent with id={} and eventId={}", userId, eventId);
+ return requestService.getRequestsByEvent(userId, eventId);
+ }
+
+ @PatchMapping("/{userId}/events/{eventId}/requests")
+ public EventRequestStatusUpdateResult updateRequestStatus(
+ @PathVariable("userId") Long userId,
+ @PathVariable("eventId") Long eventId,
+ @RequestBody EventRequestStatusUpdateRequest request
+ ) {
+ log.debug("Controller: updateRequestStatus with id={} and eventId={} with data {}", userId, eventId, request);
+ return requestService.updateRequestStatus(userId, eventId, request);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/controller/PublicEventController.java b/main-service/src/main/java/ru/practicum/event/controller/PublicEventController.java
new file mode 100644
index 0000000..8d23014
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/controller/PublicEventController.java
@@ -0,0 +1,40 @@
+package ru.practicum.event.controller;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.EventShortDto;
+import ru.practicum.event.dto.SearchEventPublicRequest;
+import ru.practicum.event.service.EventService;
+
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/events")
+@RequiredArgsConstructor
+public class PublicEventController {
+ private final EventService eventService;
+
+ @GetMapping
+ public List getEventsPublic(
+ @ModelAttribute @Valid SearchEventPublicRequest request,
+ HttpServletRequest httpRequest
+ ) {
+ int size = (request.size() != null && request.size() > 0) ? request.size() : 10;
+ int from = request.from() != null ? request.from() : 0;
+ PageRequest pageRequest = PageRequest.of(from / size, size);
+ log.debug("Controller: getEventsPublic with filters {}", request);
+ return eventService.getEventsPublic(request, pageRequest, httpRequest.getRemoteAddr());
+ }
+
+ @GetMapping("/{id}")
+ public EventFullDto getEventByIdPublic(@PathVariable Long id, HttpServletRequest request) {
+ log.debug("Controller: getEventByIdPublic with id={}", id);
+ return eventService.getEventByIdPublic(id, request.getRemoteAddr());
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/EventFullDto.java b/main-service/src/main/java/ru/practicum/event/dto/EventFullDto.java
index 7eb2d7a..4c23732 100644
--- a/main-service/src/main/java/ru/practicum/event/dto/EventFullDto.java
+++ b/main-service/src/main/java/ru/practicum/event/dto/EventFullDto.java
@@ -2,8 +2,7 @@
import com.fasterxml.jackson.annotation.JsonFormat;
import ru.practicum.category.dto.CategoryDto;
-import ru.practicum.event.EventState;
-import ru.practicum.location.dto.LocationDto;
+import ru.practicum.event.model.EventState;
import ru.practicum.user.dto.UserShortDto;
import java.time.LocalDateTime;
@@ -17,12 +16,12 @@ public record EventFullDto(
Long confirmedRequests,
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime createdOn,
String description,
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime eventDate,
UserShortDto initiator,
@@ -33,7 +32,7 @@ public record EventFullDto(
Integer participantLimit,
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime publishedOn,
boolean requestModeration,
diff --git a/main-service/src/main/java/ru/practicum/event/dto/EventShortDto.java b/main-service/src/main/java/ru/practicum/event/dto/EventShortDto.java
index 2bf3dd1..a9e36e3 100644
--- a/main-service/src/main/java/ru/practicum/event/dto/EventShortDto.java
+++ b/main-service/src/main/java/ru/practicum/event/dto/EventShortDto.java
@@ -15,7 +15,7 @@ public record EventShortDto(
Long confirmedRequests,
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime eventDate,
UserShortDto initiator,
diff --git a/main-service/src/main/java/ru/practicum/location/dto/LocationDto.java b/main-service/src/main/java/ru/practicum/event/dto/LocationDto.java
similarity index 88%
rename from main-service/src/main/java/ru/practicum/location/dto/LocationDto.java
rename to main-service/src/main/java/ru/practicum/event/dto/LocationDto.java
index 672dbf7..4bc8e89 100644
--- a/main-service/src/main/java/ru/practicum/location/dto/LocationDto.java
+++ b/main-service/src/main/java/ru/practicum/event/dto/LocationDto.java
@@ -1,4 +1,4 @@
-package ru.practicum.location.dto;
+package ru.practicum.event.dto;
import jakarta.validation.constraints.NotNull;
diff --git a/main-service/src/main/java/ru/practicum/event/dto/NewEventDto.java b/main-service/src/main/java/ru/practicum/event/dto/NewEventDto.java
index 7a3cb42..75722b5 100644
--- a/main-service/src/main/java/ru/practicum/event/dto/NewEventDto.java
+++ b/main-service/src/main/java/ru/practicum/event/dto/NewEventDto.java
@@ -2,7 +2,6 @@
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.*;
-import ru.practicum.location.dto.LocationDto;
import java.time.LocalDateTime;
@@ -20,7 +19,7 @@ public record NewEventDto(
@NotNull(message = "Дата события не может быть null")
@Future(message = "Дата события должна быть в будущем")
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime eventDate,
@NotNull(message = "Место проведения события не может быть null")
diff --git a/main-service/src/main/java/ru/practicum/event/dto/SearchEventAdminRequest.java b/main-service/src/main/java/ru/practicum/event/dto/SearchEventAdminRequest.java
new file mode 100644
index 0000000..6254df8
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/SearchEventAdminRequest.java
@@ -0,0 +1,25 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public record SearchEventAdminRequest(
+ List users,
+
+ List states,
+
+ List categories,
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
+ LocalDateTime rangeStart,
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
+ LocalDateTime rangeEnd,
+
+ Integer from,
+
+ Integer size
+) {
+}
\ No newline at end of file
diff --git a/main-service/src/main/java/ru/practicum/event/dto/SearchEventPublicRequest.java b/main-service/src/main/java/ru/practicum/event/dto/SearchEventPublicRequest.java
new file mode 100644
index 0000000..506aefb
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/SearchEventPublicRequest.java
@@ -0,0 +1,29 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public record SearchEventPublicRequest(
+ String text,
+
+ List categories,
+
+ Boolean paid,
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
+ LocalDateTime rangeStart,
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
+ LocalDateTime rangeEnd,
+
+ boolean onlyAvailable,
+
+ String sort,
+
+ Integer from,
+
+ Integer size
+) {
+}
\ No newline at end of file
diff --git a/main-service/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java b/main-service/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java
index 14427c2..41fa70e 100644
--- a/main-service/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java
+++ b/main-service/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java
@@ -4,8 +4,7 @@
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
-import ru.practicum.event.StateActionAdmin;
-import ru.practicum.location.dto.LocationDto;
+import ru.practicum.event.model.StateActionAdmin;
import java.time.LocalDateTime;
@@ -19,7 +18,7 @@ public record UpdateEventAdminRequest(
String description,
@Future(message = "Дата события должна быть в будущем")
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime eventDate,
LocationDto location,
diff --git a/main-service/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java b/main-service/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java
index 3e00fc6..cb7b228 100644
--- a/main-service/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java
+++ b/main-service/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java
@@ -4,8 +4,7 @@
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
-import ru.practicum.event.StateActionUser;
-import ru.practicum.location.dto.LocationDto;
+import ru.practicum.event.model.StateActionUser;
import java.time.LocalDateTime;
@@ -19,7 +18,7 @@ public record UpdateEventUserRequest(
String description,
@Future(message = "Дата события должна быть в будущем")
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime eventDate,
LocationDto location,
diff --git a/main-service/src/main/java/ru/practicum/event/mapper/EventMapper.java b/main-service/src/main/java/ru/practicum/event/mapper/EventMapper.java
new file mode 100644
index 0000000..76e9b50
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/mapper/EventMapper.java
@@ -0,0 +1,38 @@
+package ru.practicum.event.mapper;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.MappingTarget;
+import ru.practicum.event.dto.*;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.model.EventState;
+import ru.practicum.user.model.User;
+
+@Mapper(componentModel = "spring", uses = {LocationMapper.class})
+public interface EventMapper {
+
+ @Mapping(target = "id", ignore = true)
+ @Mapping(target = "publishedOn", ignore = true)
+ @Mapping(target = "category", ignore = true) //TODO убрать после реализации Category
+ Event fromNewEvent(NewEventDto dto, User initiator, EventState state);
+
+ EventShortDto toEventShortDto(Event event, Long confirmedRequests, Long views);
+
+ EventFullDto toEventFullDto(Event event, Long confirmedRequests, Long views);
+
+// @Mapping(target = "id", ignore = true)
+// @Mapping(target = "initiator", ignore = true)
+// @Mapping(target = "createdOn", ignore = true)
+// @Mapping(target = "publishedOn", ignore = true)
+// @Mapping(target = "state", ignore = true)
+ @Mapping(target = "category", ignore = true)
+ void updateEventFromUserRequest(UpdateEventUserRequest request, @MappingTarget Event event);
+
+// @Mapping(target = "id", ignore = true)
+// @Mapping(target = "initiator", ignore = true)
+// @Mapping(target = "createdOn", ignore = true)
+// @Mapping(target = "publishedOn", ignore = true)
+// @Mapping(target = "state", ignore = true)
+ @Mapping(target = "category", ignore = true)
+ void updateEventFromAdminRequest(UpdateEventAdminRequest request, @MappingTarget Event event);
+}
diff --git a/main-service/src/main/java/ru/practicum/event/mapper/LocationMapper.java b/main-service/src/main/java/ru/practicum/event/mapper/LocationMapper.java
new file mode 100644
index 0000000..1aac2bf
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/mapper/LocationMapper.java
@@ -0,0 +1,12 @@
+package ru.practicum.event.mapper;
+
+import org.mapstruct.Mapper;
+import ru.practicum.event.dto.LocationDto;
+import ru.practicum.event.model.EventLocation;
+
+@Mapper(componentModel = "spring")
+public interface LocationMapper {
+ EventLocation toEventLocation(LocationDto dto);
+
+ LocationDto toDto(EventLocation entity);
+}
diff --git a/main-service/src/main/java/ru/practicum/event/model/Event.java b/main-service/src/main/java/ru/practicum/event/model/Event.java
new file mode 100644
index 0000000..152babc
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/model/Event.java
@@ -0,0 +1,83 @@
+package ru.practicum.event.model;
+
+import jakarta.persistence.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import org.hibernate.annotations.CreationTimestamp;
+import ru.practicum.category.model.Category;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "events")
+@Getter
+@Setter
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class Event {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
+ private Long id;
+
+ @Column(nullable = false, length = 120)
+ private String title;
+
+ @Column(nullable = false, length = 2000)
+ private String annotation;
+
+ @Column(nullable = false, length = 7000)
+ private String description;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "category_id")
+ private Category category;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "initiator_id")
+ private User initiator;
+
+ @Column(name = "created_on", nullable = false)
+ @CreationTimestamp
+ private LocalDateTime createdOn;
+
+ @Column(name = "event_date", nullable = false)
+ private LocalDateTime eventDate;
+
+ @Column(name = "published_on")
+ private LocalDateTime publishedOn;
+
+ @Embedded
+ @AttributeOverrides({
+ @AttributeOverride(name = "lat", column = @Column(name = "lat")),
+ @AttributeOverride(name = "lon", column = @Column(name = "lon"))
+ })
+ private EventLocation location;
+
+ @Column(nullable = false)
+ private boolean paid;
+
+ @Column(name = "participant_limit", nullable = false)
+ private Integer participantLimit;
+
+ @Column(name = "request_moderation", nullable = false)
+ private boolean requestModeration = true;
+
+ @Column(nullable = false, length = 20)
+ @Enumerated(EnumType.STRING)
+ private EventState state;
+
+ public void pending() {
+ this.state = EventState.PENDING;
+ }
+
+ public void canceled() {
+ this.state = EventState.CANCELED;
+ }
+
+ public void publish() {
+ this.state = EventState.PUBLISHED;
+ this.publishedOn = LocalDateTime.now();
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/model/EventLocation.java b/main-service/src/main/java/ru/practicum/event/model/EventLocation.java
new file mode 100644
index 0000000..64f8396
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/model/EventLocation.java
@@ -0,0 +1,17 @@
+package ru.practicum.event.model;
+
+import jakarta.persistence.Embeddable;
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+@Embeddable
+@Getter
+@Setter
+public class EventLocation {
+ @NotNull(message = "Широта не может быть null")
+ private Float lat;
+
+ @NotNull(message = "Долгота не может быть null")
+ private Float lon;
+}
diff --git a/main-service/src/main/java/ru/practicum/event/EventState.java b/main-service/src/main/java/ru/practicum/event/model/EventState.java
similarity index 66%
rename from main-service/src/main/java/ru/practicum/event/EventState.java
rename to main-service/src/main/java/ru/practicum/event/model/EventState.java
index ef8e1a0..9cfbb30 100644
--- a/main-service/src/main/java/ru/practicum/event/EventState.java
+++ b/main-service/src/main/java/ru/practicum/event/model/EventState.java
@@ -1,4 +1,4 @@
-package ru.practicum.event;
+package ru.practicum.event.model;
public enum EventState {
PENDING,
diff --git a/main-service/src/main/java/ru/practicum/event/model/EventWithCountConfirmedRequests.java b/main-service/src/main/java/ru/practicum/event/model/EventWithCountConfirmedRequests.java
new file mode 100644
index 0000000..661f8e2
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/model/EventWithCountConfirmedRequests.java
@@ -0,0 +1,15 @@
+package ru.practicum.event.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class EventWithCountConfirmedRequests {
+ private Long eventId;
+ private Long countConfirmedRequests;
+}
diff --git a/main-service/src/main/java/ru/practicum/event/StateActionAdmin.java b/main-service/src/main/java/ru/practicum/event/model/StateActionAdmin.java
similarity index 67%
rename from main-service/src/main/java/ru/practicum/event/StateActionAdmin.java
rename to main-service/src/main/java/ru/practicum/event/model/StateActionAdmin.java
index d7b8901..1306769 100644
--- a/main-service/src/main/java/ru/practicum/event/StateActionAdmin.java
+++ b/main-service/src/main/java/ru/practicum/event/model/StateActionAdmin.java
@@ -1,4 +1,4 @@
-package ru.practicum.event;
+package ru.practicum.event.model;
public enum StateActionAdmin {
PUBLISH_EVENT,
diff --git a/main-service/src/main/java/ru/practicum/event/StateActionUser.java b/main-service/src/main/java/ru/practicum/event/model/StateActionUser.java
similarity index 67%
rename from main-service/src/main/java/ru/practicum/event/StateActionUser.java
rename to main-service/src/main/java/ru/practicum/event/model/StateActionUser.java
index 4640af1..e629286 100644
--- a/main-service/src/main/java/ru/practicum/event/StateActionUser.java
+++ b/main-service/src/main/java/ru/practicum/event/model/StateActionUser.java
@@ -1,4 +1,4 @@
-package ru.practicum.event;
+package ru.practicum.event.model;
public enum StateActionUser {
SEND_TO_REVIEW,
diff --git a/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java b/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java
new file mode 100644
index 0000000..55f79fe
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java
@@ -0,0 +1,37 @@
+package ru.practicum.event.repository;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import ru.practicum.event.model.Event;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.Optional;
+import java.util.List;
+
+public interface EventRepository extends JpaRepository, JpaSpecificationExecutor {
+
+ Page findAllByInitiatorIdOrderByCreatedOnDesc(Long initiatorId, Pageable pageable);
+
+ Event findFirstByOrderByCreatedOnAsc();
+
+ Optional findByIdAndInitiatorId(Long eventId, Long initiatorId);
+
+ @Query("""
+ SELECT e FROM Event e
+ LEFT JOIN FETCH e.category
+ LEFT JOIN FETCH e.initiator
+ WHERE e.id IN :eventIds"""
+ )
+ List findAllByEventIds(@Param("eventIds") List eventIds);
+
+ @Query("""
+ SELECT e FROM Event e
+ LEFT JOIN FETCH e.category
+ LEFT JOIN FETCH e.initiator
+ WHERE e.id = :eventId"""
+ )
+ Optional findByIdNew(@Param("eventId") Long eventId);
+}
diff --git a/main-service/src/main/java/ru/practicum/event/repository/SearchEventSpecifications.java b/main-service/src/main/java/ru/practicum/event/repository/SearchEventSpecifications.java
new file mode 100644
index 0000000..be4a9cb
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/repository/SearchEventSpecifications.java
@@ -0,0 +1,78 @@
+package ru.practicum.event.repository;
+
+import org.springframework.data.jpa.domain.Specification;
+import ru.practicum.event.model.Event;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public class SearchEventSpecifications {
+
+ public static Specification addWhereNull() {
+ return (root, query, criteriaBuilder) -> null;
+ }
+
+ public static Specification addWhereUsers(List userIds) {
+ return (root, query, criteriaBuilder) -> {
+ if (userIds == null || userIds.isEmpty()) return criteriaBuilder.conjunction();
+ return root.get("initiator").get("id").in(userIds);
+ };
+ }
+
+ public static Specification addWhereStates(List states) {
+ return (root, query, criteriaBuilder) -> {
+ if (states == null || states.isEmpty()) return criteriaBuilder.conjunction();
+ return root.get("state").in(states);
+ };
+ }
+
+ public static Specification addWhereCategories(List categoryIds) {
+ return (root, query, criteriaBuilder) -> {
+ if (categoryIds == null || categoryIds.isEmpty()) return criteriaBuilder.conjunction();
+ return root.get("category").get("id").in(categoryIds);
+ };
+ }
+
+ public static Specification addWhereStartsBefore(LocalDateTime dateTime) {
+ return (root, query, criteriaBuilder) -> {
+ if (dateTime == null) return criteriaBuilder.conjunction();
+ return criteriaBuilder.greaterThanOrEqualTo(root.get("eventDate"), dateTime);
+ };
+ }
+
+ public static Specification addWhereEndsAfter(LocalDateTime dateTime) {
+ return (root, query, criteriaBuilder) -> {
+ if (dateTime == null) return criteriaBuilder.conjunction();
+ return criteriaBuilder.lessThanOrEqualTo(root.get("eventDate"), dateTime);
+ };
+ }
+
+ public static Specification addLikeText(String text) {
+ return (root, query, criteriaBuilder) -> {
+ if (text == null || text.trim().isEmpty()) return criteriaBuilder.conjunction();
+ String searchText = "%" + text.toLowerCase() + "%";
+ return criteriaBuilder.or(
+ criteriaBuilder.like(criteriaBuilder.lower(root.get("annotation")), searchText),
+ criteriaBuilder.like(criteriaBuilder.lower(root.get("description")), searchText)
+ );
+ };
+ }
+
+ public static Specification isPaid(Boolean paid) {
+ return (root, query, criteriaBuilder) -> {
+ if (paid == null) return criteriaBuilder.conjunction();
+ return criteriaBuilder.equal(root.get("paid"), paid);
+ };
+ }
+
+ public static Specification addWhereAvailableSlots() {
+ return (root, query, criteriaBuilder) ->
+ criteriaBuilder.or(
+ criteriaBuilder.equal(root.get("participantLimit"), 0),
+ criteriaBuilder.greaterThan(
+ root.get("participantLimit"),
+ root.get("confirmedRequests")
+ )
+ );
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/service/EventService.java b/main-service/src/main/java/ru/practicum/event/service/EventService.java
new file mode 100644
index 0000000..774dcf0
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/service/EventService.java
@@ -0,0 +1,27 @@
+package ru.practicum.event.service;
+
+import org.springframework.data.domain.Pageable;
+import ru.practicum.event.dto.*;
+import ru.practicum.event.model.Event;
+
+import java.util.List;
+
+public interface EventService {
+ List getEvents(Long userId, Pageable pageable);
+
+ EventFullDto createEvent(Long userId, NewEventDto newEventDto);
+
+ EventFullDto getEvent(Long userId, Long eventId, String ip);
+
+ EventFullDto updateEvent(Long userId, Long eventId, UpdateEventUserRequest request);
+
+ Event getEventOrThrow(Long eventId, Long userId);
+
+ List getEventsAdmin(SearchEventAdminRequest request, Pageable pageable);
+
+ EventFullDto updateEventAdmin(Long eventId, UpdateEventAdminRequest request);
+
+ List getEventsPublic(SearchEventPublicRequest requestParams, Pageable pageable, String ip);
+
+ EventFullDto getEventByIdPublic(Long eventId, String ip);
+}
diff --git a/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java b/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java
new file mode 100644
index 0000000..557b0bf
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java
@@ -0,0 +1,329 @@
+package ru.practicum.event.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.CreateEndpointHitDto;
+import ru.practicum.StatsClient;
+import ru.practicum.ViewStatsDto;
+import ru.practicum.event.dto.*;
+import ru.practicum.event.mapper.EventMapper;
+import ru.practicum.event.model.*;
+import ru.practicum.event.repository.EventRepository;
+import ru.practicum.event.repository.SearchEventSpecifications;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+import ru.practicum.exception.ValidationException;
+import ru.practicum.request.repository.RequestRepository;
+import ru.practicum.user.model.User;
+import ru.practicum.user.service.UserService;
+
+import java.time.LocalDateTime;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class EventServiceImpl implements EventService {
+ private final EventRepository eventRepository;
+ private final EventMapper eventMapper;
+ private final UserService userService;
+ private final RequestRepository requestRepository;
+ private final StatsClient statsClient;
+// private final CategoryService categoryService;
+
+ @Override
+ @Transactional
+ public EventFullDto createEvent(Long userId, NewEventDto newEventDto) {
+ validateDateEvent(newEventDto.eventDate(), 2);
+
+ User user = userService.getUserById(userId);
+// Category category = categoryService.getCategoryById(newEventDto.category()); TODO
+ Event event = eventMapper.fromNewEvent(newEventDto, user, EventState.PENDING);
+ log.info("Создано новое событие: {}", event);
+ return eventMapper.toEventFullDto(eventRepository.save(event), 0L, 0L);
+ }
+
+ @Override
+ public List getEvents(Long userId, Pageable pageable) {
+ userService.getUserById(userId);
+ Page eventsPage = eventRepository.findAllByInitiatorIdOrderByCreatedOnDesc(userId, pageable);
+
+ if (eventsPage.isEmpty()) return List.of();
+
+ List eventIds = eventsPage.stream()
+ .map(Event::getId)
+ .collect(Collectors.toList());
+
+ Map confirmedRequests = getConfirmedRequests(eventIds);
+ Map views = getViewsForEvents(eventIds);
+
+ return eventsPage.stream()
+ .map(event -> eventMapper.toEventShortDto(
+ event,
+ confirmedRequests.getOrDefault(event.getId(), 0L),
+ views.getOrDefault(event.getId(), 0L)
+ ))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public EventFullDto getEvent(Long userId, Long eventId, String ip) {
+ userService.getUserByIdOrThrow(userId);
+ Event event = getEventOrThrow(eventId, userId);
+
+ saveHit("/events/" + eventId, ip);
+
+ return buildFullDto(event);
+ }
+
+ @Override
+ @Transactional
+ public EventFullDto updateEvent(Long userId, Long eventId, UpdateEventUserRequest request) {
+ userService.getUserByIdOrThrow(userId);
+ Event event = getEventOrThrow(eventId, userId);
+
+ validateEventStateForUpdate(event);
+ validateDateEvent(request.eventDate(), 2);
+ validateParticipantLimit(request.participantLimit());
+
+ eventMapper.updateEventFromUserRequest(request, event);
+ updateEventState(event, request);
+
+ Event updatedEvent = eventRepository.save(event);
+ return buildFullDto(updatedEvent);
+ }
+
+ @Override
+ public Event getEventOrThrow(Long eventId, Long userId) {
+ return eventRepository.findByIdAndInitiatorId(eventId, userId)
+ .orElseThrow(() -> new NotFoundException("Событие c id " + eventId + " не найдено"));
+ }
+
+ @Override
+ public List getEventsAdmin(SearchEventAdminRequest request, Pageable pageable) {
+ validateRangeStartAndEnd(request.rangeStart(), request.rangeEnd());
+
+ Specification specification = SearchEventSpecifications.addWhereNull();
+ if (request.users() != null && !request.users().isEmpty())
+ specification = specification.and(SearchEventSpecifications.addWhereUsers(request.users()));
+ if (request.states() != null && !request.states().isEmpty())
+ specification = specification.and(SearchEventSpecifications.addWhereStates(request.states()));
+ if (request.categories() != null && !request.categories().isEmpty())
+ specification = specification.and(SearchEventSpecifications.addWhereCategories(request.categories()));
+ if (request.rangeStart() != null)
+ specification = specification.and(SearchEventSpecifications.addWhereStartsBefore(request.rangeStart()));
+ if (request.rangeEnd() != null)
+ specification = specification.and(SearchEventSpecifications.addWhereEndsAfter(request.rangeEnd()));
+ if (request.rangeStart() == null && request.rangeEnd() == null)
+ specification = specification.and(SearchEventSpecifications.addWhereStartsBefore(LocalDateTime.now()));
+
+ Page eventIdsPage = eventRepository.findAll(specification, pageable).map(Event::getId);
+ List eventIds = eventIdsPage.getContent();
+
+ if (eventIds.isEmpty()) return List.of();
+ List events = eventRepository.findAllByEventIds(eventIds);
+
+ if (events.isEmpty()) return List.of();
+ List searchEventIds = events.stream()
+ .map(Event::getId)
+ .toList();
+
+ Map confirmedRequests = getConfirmedRequests(searchEventIds);
+ Map views = getViewsForEvents(searchEventIds);
+
+ return events.stream()
+ .map(event -> eventMapper.toEventFullDto(
+ event,
+ confirmedRequests.getOrDefault(event.getId(), 0L),
+ views.getOrDefault(event.getId(), 0L)
+ ))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ @Transactional
+ public EventFullDto updateEventAdmin(Long eventId, UpdateEventAdminRequest request) {
+ Event event = eventRepository.findByIdNew(eventId)
+ .orElseThrow(() -> new NotFoundException("Событие c id " + eventId + " не найдено"));
+
+ if (request.stateAction() != null) {
+ StateActionAdmin stateActionAdmin = request.stateAction();
+ EventState currentState = event.getState();
+
+ if (stateActionAdmin == StateActionAdmin.PUBLISH_EVENT) {
+ if (currentState != EventState.PENDING)
+ throw new ConflictException("Событие можно опубликовать только если оно в состоянии ожидания публикации");
+ validateDateEvent(event.getEventDate(), 1);
+ event.publish();
+ }
+ if (stateActionAdmin == StateActionAdmin.REJECT_EVENT) {
+ if (currentState == EventState.PUBLISHED)
+ throw new ConflictException("Событие можно отклонить пока оно не опубликовано");
+ event.canceled();
+ }
+ }
+ eventMapper.updateEventFromAdminRequest(request, event);
+ Event updatedEvent = eventRepository.save(event);
+ log.info("Обновлено событие: {}", updatedEvent);
+ return buildFullDto(updatedEvent);
+ }
+
+ @Override
+ public List getEventsPublic(SearchEventPublicRequest request, Pageable pageable, String ip) {
+ validateRangeStartAndEnd(request.rangeStart(), request.rangeEnd());
+
+ Specification specification = SearchEventSpecifications.addWhereNull();
+ if (request.text() != null && !request.text().trim().isEmpty())
+ specification = specification.and(SearchEventSpecifications.addLikeText(request.text()));
+ if (request.categories() != null && !request.categories().isEmpty())
+ specification = specification.and(SearchEventSpecifications.addWhereCategories(request.categories()));
+ if (request.paid() != null)
+ specification = specification.and(SearchEventSpecifications.isPaid(request.paid()));
+ LocalDateTime rangeStart = (request.rangeStart() == null && request.rangeEnd() == null) ?
+ LocalDateTime.now() : request.rangeStart();
+ if (rangeStart != null)
+ specification = specification.and(SearchEventSpecifications.addWhereStartsBefore(rangeStart));
+ if (request.rangeEnd() != null)
+ specification = specification.and(SearchEventSpecifications.addWhereEndsAfter(request.rangeEnd()));
+ if (request.onlyAvailable())
+ specification = specification.and(SearchEventSpecifications.addWhereAvailableSlots());
+
+ List events = eventRepository.findAll(specification, pageable).getContent();
+
+ if (events.isEmpty()) return List.of();
+
+ List eventIds = events.stream()
+ .map(Event::getId)
+ .collect(Collectors.toList());
+
+ Map confirmedRequests = getConfirmedRequests(eventIds);
+ Map views = getViewsForEvents(eventIds);
+
+ List result = events.stream()
+ .map(event -> eventMapper.toEventShortDto(
+ event,
+ confirmedRequests.getOrDefault(event.getId(), 0L),
+ views.getOrDefault(event.getId(), 0L)
+ ))
+ .collect(Collectors.toList());
+
+ saveHit("/events", ip);
+
+ if ("VIEWS".equals(request.sort())) {
+ return result.stream()
+ .sorted(Comparator.comparing(EventShortDto::views).reversed())
+ .collect(Collectors.toList());
+ }
+ return result;
+ }
+
+ @Override
+ public EventFullDto getEventByIdPublic(Long eventId, String ip) {
+ Event event = eventRepository.findByIdNew(eventId)
+ .filter(ev -> ev.getState() == EventState.PUBLISHED)
+ .orElseThrow(() -> new NotFoundException("Событие c id " + eventId + " не найдено"));
+
+ saveHit("/events/" + eventId, ip);
+
+ return buildFullDto(event);
+ }
+
+ private EventFullDto buildFullDto(Event event) {
+ Map confirmedRequests = getConfirmedRequests(List.of(event.getId()));
+ Map views = getViewsForEvents(List.of(event.getId()));
+
+ return eventMapper.toEventFullDto(
+ event,
+ confirmedRequests.getOrDefault(event.getId(), 0L),
+ views.getOrDefault(event.getId(), 0L)
+ );
+ }
+
+ private void validateRangeStartAndEnd(LocalDateTime rangeStart, LocalDateTime rangeEnd) {
+ if (rangeStart != null && rangeEnd != null && rangeStart.isAfter(rangeEnd))
+ throw new ValidationException("Дата начала не может быть позже даты окончания");
+ }
+
+ private void updateEventState(Event event, UpdateEventUserRequest request) {
+ if (request.stateAction() != null) {
+ if (request.stateAction() == StateActionUser.SEND_TO_REVIEW)
+ event.pending();
+ if (request.stateAction() == StateActionUser.CANCEL_REVIEW)
+ event.canceled();
+ }
+ }
+
+ private void validateParticipantLimit(Integer limitParticipant) {
+ if (limitParticipant != null && limitParticipant < 0)
+ throw new ValidationException("Лимит участников не может быть отрицательным");
+ }
+
+ private void validateEventStateForUpdate(Event event) {
+ if (event.getState() == EventState.PUBLISHED)
+ throw new ValidationException("Изменять можно только не опубликованные события");
+ }
+
+ private void saveHit(String path, String ip) {
+ CreateEndpointHitDto endpointHitDto = new CreateEndpointHitDto("main-service", path, ip, LocalDateTime.now());
+ statsClient.saveHit(endpointHitDto);
+ }
+
+ private void validateDateEvent(LocalDateTime eventDate, long minHoursBeforeStartEvent) {
+ if (eventDate != null && eventDate.isBefore(LocalDateTime.now().plusHours(minHoursBeforeStartEvent)))
+ throw new ValidationException("Дата начала события не может быть ранее чем через " + minHoursBeforeStartEvent + " часа(ов)");
+ }
+
+ private Map getConfirmedRequests(List eventIds) {
+ if (eventIds.isEmpty()) return Map.of();
+
+ List events = requestRepository.findCountConfirmedRequestsByEventIds(eventIds);
+ Map confirmedRequests = eventIds.stream()
+ .collect(Collectors.toMap(id -> id, id -> 0L));
+ events.forEach(dto -> confirmedRequests.put(dto.getEventId(), dto.getCountConfirmedRequests()));
+
+ return confirmedRequests;
+ }
+
+ private Map getViewsForEvents(List eventIds) {
+ if (eventIds.isEmpty()) return Map.of();
+
+ List uris = eventIds.stream()
+ .map(id -> "/events/" + id)
+ .collect(Collectors.toList());
+
+ LocalDateTime start = eventRepository.findFirstByOrderByCreatedOnAsc().getCreatedOn();
+ LocalDateTime end = LocalDateTime.now();
+
+ List stats = statsClient.getStats(start, end, uris, true);
+
+ Map views = eventIds.stream()
+ .collect(Collectors.toMap(id -> id, id -> 0L));
+
+ if (stats != null && !stats.isEmpty()) {
+ stats.forEach(stat -> {
+ Long eventId = getEventIdFromUri(stat.uri());
+ if (eventId > -1L) {
+ views.put(eventId, stat.hits());
+ }
+ });
+ }
+ return views;
+ }
+
+ private Long getEventIdFromUri(String uri) {
+ try {
+ return Long.parseLong(uri.substring("/events".length() + 1));
+ } catch (Exception e) {
+ return -1L;
+ }
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/exception/BadRequestException.java b/main-service/src/main/java/ru/practicum/exception/BadRequestException.java
new file mode 100644
index 0000000..7aee4bc
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/exception/BadRequestException.java
@@ -0,0 +1,7 @@
+package ru.practicum.exception;
+
+public class BadRequestException extends RuntimeException {
+ public BadRequestException(String message) {
+ super(message);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/exception/ConflictException.java b/main-service/src/main/java/ru/practicum/exception/ConflictException.java
new file mode 100644
index 0000000..e2fddf1
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/exception/ConflictException.java
@@ -0,0 +1,7 @@
+package ru.practicum.exception;
+
+public class ConflictException extends RuntimeException {
+ public ConflictException(String message) {
+ super(message);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/exception/ErrorHandler.java b/main-service/src/main/java/ru/practicum/exception/ErrorHandler.java
new file mode 100644
index 0000000..a56cfe8
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/exception/ErrorHandler.java
@@ -0,0 +1,142 @@
+package ru.practicum.exception;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.HttpMediaTypeNotAcceptableException;
+import org.springframework.web.HttpMediaTypeNotSupportedException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingPathVariableException;
+import org.springframework.web.bind.ServletRequestBindingException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.ServletWebRequest;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
+import org.springframework.web.servlet.NoHandlerFoundException;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+@Slf4j
+@RestControllerAdvice
+public class ErrorHandler extends ResponseEntityExceptionHandler {
+
+ @ExceptionHandler(NotFoundException.class)
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public ErrorResponse handleNotFoundException(NotFoundException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.NOT_FOUND, exception, request);
+ }
+
+ @ExceptionHandler(BadRequestException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ErrorResponse handleBadRequestException(BadRequestException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.BAD_REQUEST, exception, request);
+ }
+
+ @ExceptionHandler(ForbiddenException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public ErrorResponse handleForbiddenException(ForbiddenException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.FORBIDDEN, exception, request);
+ }
+
+ @ExceptionHandler(ConflictException.class)
+ @ResponseStatus(HttpStatus.CONFLICT)
+ public ErrorResponse handleConflictException(ConflictException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.CONFLICT, exception, request);
+ }
+
+ @ExceptionHandler(ValidationException.class)
+ @ResponseStatus(HttpStatus.CONFLICT)
+ public ErrorResponse handleValidationException(ValidationException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.CONFLICT, exception, request);
+ }
+
+ @Override
+ protected ResponseEntity