diff --git a/docker-compose.yml b/docker-compose.yml
index d3e93f6..b101d78 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -31,6 +31,7 @@ services:
- "8080:8080"
depends_on:
- main-db
+ - stats-server
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://main-db:5432/maindb
- SPRING_DATASOURCE_USERNAME=dbuser2
diff --git a/main/pom.xml b/main/pom.xml
index f2f5f44..95f9b62 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -39,5 +39,48 @@
org.springframework.boot
spring-boot-starter-actuator
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
\ No newline at end of file
diff --git a/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java b/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java
new file mode 100644
index 0000000..cf4dc0a
--- /dev/null
+++ b/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java
@@ -0,0 +1,39 @@
+package ru.practicum.category.controller;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+import ru.practicum.category.service.CategoryService;
+
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/admin/categories")
+public class CategoryAdminController {
+
+ private final CategoryService categoryService;
+
+ @ResponseStatus(HttpStatus.CREATED)
+ @PostMapping
+ public CategoryDto createCategory(@Valid @RequestBody NewCategoryDto newCategoryDto) {
+ log.info("POST /admin/categories");
+ return categoryService.createCategory(newCategoryDto);
+ }
+
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @DeleteMapping("/{catId}")
+ public void deleteCategory(@PathVariable("catId") Long id) {
+ log.info("DELETE /admin/categories/{}", id);
+ categoryService.deleteCategory(id);
+ }
+
+ @PatchMapping("/{catId}")
+ public CategoryDto updateCategory(@PathVariable("catId") Long id, @Valid @RequestBody NewCategoryDto newCategoryDto) {
+ log.info("PATCH /admin/categories/{}", id);
+ return categoryService.updateCategory(id, newCategoryDto);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/category/controller/CategoryPublicController.java b/main/src/main/java/ru/practicum/category/controller/CategoryPublicController.java
new file mode 100644
index 0000000..8ab7b2e
--- /dev/null
+++ b/main/src/main/java/ru/practicum/category/controller/CategoryPublicController.java
@@ -0,0 +1,35 @@
+package ru.practicum.category.controller;
+
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.PositiveOrZero;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.service.CategoryService;
+
+import java.util.List;
+
+@Validated
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/categories")
+public class CategoryPublicController {
+
+ private final CategoryService categoryService;
+
+ @GetMapping
+ public List getCategories(@RequestParam(defaultValue = "0") @PositiveOrZero Integer from,
+ @RequestParam(defaultValue = "10") @Positive Integer size) {
+ log.info("GET /categories?from={}&size={}", from, size);
+ return categoryService.getCategories(from, size);
+ }
+
+ @GetMapping("/{catId}")
+ public CategoryDto getCategory(@PathVariable(value = "catId") Long id) {
+ log.info("GET /categories/{}", id);
+ return categoryService.getCategory(id);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/category/dto/CategoryDto.java b/main/src/main/java/ru/practicum/category/dto/CategoryDto.java
new file mode 100644
index 0000000..0423033
--- /dev/null
+++ b/main/src/main/java/ru/practicum/category/dto/CategoryDto.java
@@ -0,0 +1,15 @@
+package ru.practicum.category.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class CategoryDto {
+ private Long id;
+ private String name;
+}
diff --git a/main/src/main/java/ru/practicum/category/dto/NewCategoryDto.java b/main/src/main/java/ru/practicum/category/dto/NewCategoryDto.java
new file mode 100644
index 0000000..5a768c8
--- /dev/null
+++ b/main/src/main/java/ru/practicum/category/dto/NewCategoryDto.java
@@ -0,0 +1,18 @@
+package ru.practicum.category.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class NewCategoryDto {
+ @NotBlank(message = "Название категории не может быть пустым")
+ @Size(min = 1, max = 50, message = "Название категории должно быть в границах от {min} до {max} символов")
+ private String name;
+}
diff --git a/main/src/main/java/ru/practicum/category/dto/mapper/CategoryMapper.java b/main/src/main/java/ru/practicum/category/dto/mapper/CategoryMapper.java
new file mode 100644
index 0000000..d9b571c
--- /dev/null
+++ b/main/src/main/java/ru/practicum/category/dto/mapper/CategoryMapper.java
@@ -0,0 +1,23 @@
+package ru.practicum.category.dto.mapper;
+
+import lombok.experimental.UtilityClass;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+import ru.practicum.category.model.Category;
+
+@UtilityClass
+public class CategoryMapper {
+ public static CategoryDto toCategoryDto(Category category) {
+ return new CategoryDto(
+ category.getId(),
+ category.getName()
+ );
+ }
+
+ public static Category toCategory(NewCategoryDto newCategoryDto) {
+ return new Category(
+ null,
+ newCategoryDto.getName()
+ );
+ }
+}
diff --git a/main/src/main/java/ru/practicum/category/model/Category.java b/main/src/main/java/ru/practicum/category/model/Category.java
new file mode 100644
index 0000000..9c4c844
--- /dev/null
+++ b/main/src/main/java/ru/practicum/category/model/Category.java
@@ -0,0 +1,34 @@
+package ru.practicum.category.model;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.Objects;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+@Table(name = "categories")
+@Entity
+public class Category {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+ private String name;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Category category)) return false;
+ return Objects.equals(id, category.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java b/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java
new file mode 100644
index 0000000..5af656e
--- /dev/null
+++ b/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java
@@ -0,0 +1,12 @@
+package ru.practicum.category.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.practicum.category.model.Category;
+
+import java.util.Optional;
+
+public interface CategoryRepository extends JpaRepository {
+ boolean existsByName(String name);
+
+ Optional findByName(String name);
+}
diff --git a/main/src/main/java/ru/practicum/category/service/CategoryService.java b/main/src/main/java/ru/practicum/category/service/CategoryService.java
new file mode 100644
index 0000000..813ad36
--- /dev/null
+++ b/main/src/main/java/ru/practicum/category/service/CategoryService.java
@@ -0,0 +1,18 @@
+package ru.practicum.category.service;
+
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+
+import java.util.List;
+
+public interface CategoryService {
+ CategoryDto createCategory(NewCategoryDto newCategoryDto);
+
+ void deleteCategory(Long id);
+
+ CategoryDto updateCategory(Long id, NewCategoryDto newCategoryDto);
+
+ List getCategories(Integer from, Integer size);
+
+ CategoryDto getCategory(Long id);
+}
diff --git a/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java
new file mode 100644
index 0000000..059062e
--- /dev/null
+++ b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java
@@ -0,0 +1,97 @@
+package ru.practicum.category.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+import ru.practicum.category.dto.mapper.CategoryMapper;
+import ru.practicum.category.model.Category;
+import ru.practicum.category.repository.CategoryRepository;
+import ru.practicum.event.repository.EventRepository;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Transactional(readOnly = true)
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class CategoryServiceImpl implements CategoryService {
+ private final CategoryRepository categoryRepository;
+ private final EventRepository eventRepository;
+
+ @Transactional
+ @Override
+ public CategoryDto createCategory(NewCategoryDto newCategoryDto) {
+ if (categoryRepository.existsByName(newCategoryDto.getName())) {
+ log.warn("Категория - {} уже существует", newCategoryDto.getName());
+ throw new ConflictException("Категория - " + newCategoryDto.getName() + " уже существует");
+ }
+
+ return CategoryMapper.toCategoryDto(categoryRepository.save(CategoryMapper.toCategory(newCategoryDto)));
+ }
+
+ @Transactional
+ @Override
+ public void deleteCategory(Long id) {
+ findCategoryById(id);
+ validateCategoryIsEmpty(id);
+ categoryRepository.deleteById(id);
+ }
+
+ @Transactional
+ @Override
+ public CategoryDto updateCategory(Long id, NewCategoryDto newCategoryDto) {
+ Category category = findCategoryById(id);
+
+ if (newCategoryDto.getName() != null && !category.getName().equals(newCategoryDto.getName())) {
+ categoryRepository.findByName(newCategoryDto.getName())
+ .ifPresent(existing -> {
+ if (!existing.getId().equals(id)) {
+ log.warn("Категория с именем '{}' уже существует", newCategoryDto.getName());
+ throw new ConflictException("Категория с именем '" + newCategoryDto.getName() + "' уже существует");
+ }
+ });
+
+ category.setName(newCategoryDto.getName());
+ }
+
+ return CategoryMapper.toCategoryDto(categoryRepository.save(category));
+ }
+
+ @Override
+ public List getCategories(Integer from, Integer size) {
+ Pageable pageable = PageRequest.of(from / size, size, Sort.by("id").ascending());
+
+ return categoryRepository.findAll(pageable)
+ .stream()
+ .map(CategoryMapper::toCategoryDto)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public CategoryDto getCategory(Long id) {
+ return CategoryMapper.toCategoryDto(findCategoryById(id));
+ }
+
+ private Category findCategoryById(Long categoryId) {
+ return categoryRepository.findById(categoryId)
+ .orElseThrow(() -> new NotFoundException("Категория с данным id: " + categoryId + " не найдена"));
+ }
+
+ private void validateCategoryIsEmpty(Long categoryId) {
+ boolean isUsed = eventRepository.existsByCategoryId(categoryId);
+
+ if (isUsed) {
+ log.warn("Категория с данным id: {} используется", categoryId);
+ throw new ConflictException("Категория используется");
+ }
+ }
+}
diff --git a/main/src/main/java/ru/practicum/compilation/controller/CompilationAdminController.java b/main/src/main/java/ru/practicum/compilation/controller/CompilationAdminController.java
new file mode 100644
index 0000000..42e3c46
--- /dev/null
+++ b/main/src/main/java/ru/practicum/compilation/controller/CompilationAdminController.java
@@ -0,0 +1,40 @@
+package ru.practicum.compilation.controller;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.dto.NewCompilationDto;
+import ru.practicum.compilation.dto.UpdateCompilationRequest;
+import ru.practicum.compilation.service.CompilationService;
+
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/admin/compilations")
+public class CompilationAdminController {
+ private final CompilationService compilationService;
+
+ @ResponseStatus(HttpStatus.CREATED)
+ @PostMapping
+ public CompilationDto createCompilation(@RequestBody @Valid NewCompilationDto newCompilationDto) {
+ log.info("POST /admin/compilations");
+ return compilationService.createCompilation(newCompilationDto);
+ }
+
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @DeleteMapping("/{compId}")
+ public void deleteCompilation(@PathVariable("compId") Long id) {
+ log.info("DELETE /admin/compilations");
+ compilationService.deleteCompilation(id);
+ }
+
+ @PatchMapping("/{compId}")
+ public CompilationDto updateCompilation(@PathVariable("compId") Long id,
+ @RequestBody(required = false) @Valid UpdateCompilationRequest updateCompilationRequest) {
+ log.info("PATCH /admin/compilations/{}", id);
+ return compilationService.updateCompilation(id, updateCompilationRequest);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/compilation/controller/CompilationPublicController.java b/main/src/main/java/ru/practicum/compilation/controller/CompilationPublicController.java
new file mode 100644
index 0000000..d46b6b8
--- /dev/null
+++ b/main/src/main/java/ru/practicum/compilation/controller/CompilationPublicController.java
@@ -0,0 +1,35 @@
+package ru.practicum.compilation.controller;
+
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.PositiveOrZero;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.service.CompilationService;
+
+import java.util.List;
+
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/compilations")
+public class CompilationPublicController {
+ private final CompilationService compilationService;
+
+ @GetMapping
+ @ResponseStatus(HttpStatus.OK)
+ public List getCompilations(@RequestParam(required = false) Boolean pinned,
+ @RequestParam(defaultValue = "0") @PositiveOrZero Integer from,
+ @RequestParam(defaultValue = "10") @Positive Integer size) {
+ log.info("GET /compilations");
+ return compilationService.getCompilations(pinned, from, size);
+ }
+
+ @GetMapping("/{compId}")
+ public CompilationDto getCompilationById(@PathVariable("compId") Long id) {
+ log.info("GET /compilations/{}", id);
+ return compilationService.getCompilationById(id);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/compilation/dto/CompilationDto.java b/main/src/main/java/ru/practicum/compilation/dto/CompilationDto.java
new file mode 100644
index 0000000..c4ab68d
--- /dev/null
+++ b/main/src/main/java/ru/practicum/compilation/dto/CompilationDto.java
@@ -0,0 +1,20 @@
+package ru.practicum.compilation.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.event.dto.EventShortDto;
+
+import java.util.Set;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class CompilationDto {
+ private Long id;
+ private String title;
+ private Boolean pinned;
+ private Set events;
+}
diff --git a/main/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java b/main/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java
new file mode 100644
index 0000000..7f6d1d1
--- /dev/null
+++ b/main/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java
@@ -0,0 +1,24 @@
+package ru.practicum.compilation.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.Set;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class NewCompilationDto {
+ private Set events;
+
+ private Boolean pinned;
+
+ @NotBlank(message = "Заголовок не может быть пустым")
+ @Size(min = 1, max = 50, message = "Заголовок должен быть в границах от {min} до {max} символов")
+ private String title;
+}
diff --git a/main/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java b/main/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java
new file mode 100644
index 0000000..61ffca8
--- /dev/null
+++ b/main/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java
@@ -0,0 +1,22 @@
+package ru.practicum.compilation.dto;
+
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.Set;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class UpdateCompilationRequest {
+ private Set events;
+
+ private Boolean pinned;
+
+ @Size(min = 1, max = 50, message = "Заголовок должен быть в границах от {min} до {max} символов")
+ private String title;
+}
diff --git a/main/src/main/java/ru/practicum/compilation/dto/mapper/CompilationMapper.java b/main/src/main/java/ru/practicum/compilation/dto/mapper/CompilationMapper.java
new file mode 100644
index 0000000..b39e8c7
--- /dev/null
+++ b/main/src/main/java/ru/practicum/compilation/dto/mapper/CompilationMapper.java
@@ -0,0 +1,34 @@
+package ru.practicum.compilation.dto.mapper;
+
+import lombok.experimental.UtilityClass;
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.dto.NewCompilationDto;
+import ru.practicum.compilation.model.Compilation;
+import ru.practicum.event.dto.mapper.EventMapper;
+
+import java.util.HashSet;
+import java.util.stream.Collectors;
+
+@UtilityClass
+public class CompilationMapper {
+ public static CompilationDto toCompilationDto(Compilation compilation) {
+ return new CompilationDto(
+ compilation.getId(),
+ compilation.getTitle(),
+ compilation.getPinned(),
+ compilation.getEvents()
+ .stream()
+ .map(EventMapper::toEventShortDto)
+ .collect(Collectors.toSet())
+ );
+ }
+
+ public static Compilation toCompilation(NewCompilationDto newCompilationDto) {
+ return new Compilation(
+ null,
+ newCompilationDto.getTitle(),
+ newCompilationDto.getPinned() != null ? newCompilationDto.getPinned() : false,
+ new HashSet<>()
+ );
+ }
+}
diff --git a/main/src/main/java/ru/practicum/compilation/model/Compilation.java b/main/src/main/java/ru/practicum/compilation/model/Compilation.java
new file mode 100644
index 0000000..feac846
--- /dev/null
+++ b/main/src/main/java/ru/practicum/compilation/model/Compilation.java
@@ -0,0 +1,50 @@
+package ru.practicum.compilation.model;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.event.model.Event;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+@Entity
+@Table(name = "compilations")
+public class Compilation {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(length = 50, nullable = false)
+ private String title;
+
+ @Column(nullable = false)
+ private Boolean pinned = false;
+
+ @ManyToMany
+ @JoinTable(
+ name = "compilations_events",
+ joinColumns = @JoinColumn(name = "compilation_id"),
+ inverseJoinColumns = @JoinColumn(name = "event_id")
+ )
+ private Set events = new HashSet<>();
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Compilation compilation)) return false;
+ return Objects.equals(id, compilation.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java b/main/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java
new file mode 100644
index 0000000..d75872d
--- /dev/null
+++ b/main/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java
@@ -0,0 +1,10 @@
+package ru.practicum.compilation.repository;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.practicum.compilation.model.Compilation;
+
+public interface CompilationRepository extends JpaRepository {
+ Page findAllByPinned(boolean pinned, Pageable pageable);
+}
diff --git a/main/src/main/java/ru/practicum/compilation/service/CompilationService.java b/main/src/main/java/ru/practicum/compilation/service/CompilationService.java
new file mode 100644
index 0000000..641a966
--- /dev/null
+++ b/main/src/main/java/ru/practicum/compilation/service/CompilationService.java
@@ -0,0 +1,19 @@
+package ru.practicum.compilation.service;
+
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.dto.NewCompilationDto;
+import ru.practicum.compilation.dto.UpdateCompilationRequest;
+
+import java.util.List;
+
+public interface CompilationService {
+ CompilationDto createCompilation(NewCompilationDto newCompilationDto);
+
+ void deleteCompilation(Long id);
+
+ CompilationDto updateCompilation(Long id, UpdateCompilationRequest updateCompilationRequest);
+
+ List getCompilations(Boolean pinned, Integer from, Integer size);
+
+ CompilationDto getCompilationById(Long id);
+}
diff --git a/main/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java b/main/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java
new file mode 100644
index 0000000..f82c1a9
--- /dev/null
+++ b/main/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java
@@ -0,0 +1,110 @@
+package ru.practicum.compilation.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.dto.NewCompilationDto;
+import ru.practicum.compilation.dto.UpdateCompilationRequest;
+import ru.practicum.compilation.dto.mapper.CompilationMapper;
+import ru.practicum.compilation.model.Compilation;
+import ru.practicum.compilation.repository.CompilationRepository;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.repository.EventRepository;
+import ru.practicum.exception.NotFoundException;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Transactional(readOnly = true)
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class CompilationServiceImpl implements CompilationService {
+ private final CompilationRepository compilationRepository;
+ private final EventRepository eventRepository;
+
+ @Transactional
+ @Override
+ public CompilationDto createCompilation(NewCompilationDto newCompilationDto) {
+ Compilation compilation = CompilationMapper.toCompilation(newCompilationDto);
+
+ if (newCompilationDto.getEvents() != null && !newCompilationDto.getEvents().isEmpty()) {
+ Set events = new HashSet<>(eventRepository.findAllById(newCompilationDto.getEvents()));
+ compilation.setEvents(events);
+ }
+
+ return CompilationMapper.toCompilationDto(compilationRepository.save(compilation));
+ }
+
+ @Transactional
+ @Override
+ public void deleteCompilation(Long id) {
+ if (!compilationRepository.existsById(id)) {
+ log.warn("Подборка с id - {} не найдена", id);
+ throw new NotFoundException("Подборка с id - " + id + " не найдена");
+ }
+ compilationRepository.deleteById(id);
+ }
+
+ @Transactional
+ @Override
+ public CompilationDto updateCompilation(Long id, UpdateCompilationRequest updateCompilationRequest) {
+ Compilation compilation = validateCompilation(id);
+
+ if (updateCompilationRequest == null) {
+ return CompilationMapper.toCompilationDto(compilationRepository.findById(id).get());
+ }
+
+ if (updateCompilationRequest.getEvents() != null) {
+ Set events = new HashSet<>(
+ eventRepository.findAllById(updateCompilationRequest.getEvents())
+ );
+ compilation.setEvents(events);
+ }
+
+ if (updateCompilationRequest.getPinned() != null) {
+ compilation.setPinned(updateCompilationRequest.getPinned());
+ }
+
+ if (updateCompilationRequest.getTitle() != null) {
+ compilation.setTitle(updateCompilationRequest.getTitle());
+ }
+
+ return CompilationMapper.toCompilationDto(compilationRepository.save(compilation));
+ }
+
+ @Override
+ public List getCompilations(Boolean pinned, Integer from, Integer size) {
+ Pageable pageable = PageRequest.of(from / size, size);
+
+ Page compilations;
+ if (pinned != null) {
+ compilations = compilationRepository.findAllByPinned(pinned, pageable);
+ } else {
+ compilations = compilationRepository.findAll(pageable);
+ }
+
+ return compilations.stream()
+ .map(CompilationMapper::toCompilationDto)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public CompilationDto getCompilationById(Long id) {
+ Compilation compilation = validateCompilation(id);
+
+ return CompilationMapper.toCompilationDto(compilation);
+ }
+
+ private Compilation validateCompilation(Long id) {
+ return compilationRepository.findById(id)
+ .orElseThrow(() -> new NotFoundException("Подборка с id:" + id + " не найдена"));
+ }
+}
diff --git a/main/src/main/java/ru/practicum/error/ApiError.java b/main/src/main/java/ru/practicum/error/ApiError.java
new file mode 100644
index 0000000..327ff47
--- /dev/null
+++ b/main/src/main/java/ru/practicum/error/ApiError.java
@@ -0,0 +1,19 @@
+package ru.practicum.error;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@AllArgsConstructor
+@Getter
+public class ApiError {
+ private final List errors;
+ private final String message;
+ private final String reason;
+ private final String status;
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private final LocalDateTime timestamp;
+}
diff --git a/main/src/main/java/ru/practicum/error/ErrorHandler.java b/main/src/main/java/ru/practicum/error/ErrorHandler.java
new file mode 100644
index 0000000..e7980c8
--- /dev/null
+++ b/main/src/main/java/ru/practicum/error/ErrorHandler.java
@@ -0,0 +1,103 @@
+package ru.practicum.error;
+
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+import ru.practicum.exception.ValidationException;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.stream.Collectors;
+
+@Slf4j
+@RestControllerAdvice
+public class ErrorHandler {
+
+ @ExceptionHandler(NotFoundException.class)
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public ApiError handleNotFoundException(NotFoundException e) {
+ return new ApiError(
+ Collections.emptyList(),
+ e.getMessage(),
+ "Not found.",
+ "NOT_FOUND",
+ LocalDateTime.now()
+ );
+ }
+
+ @ExceptionHandler({MethodArgumentNotValidException.class,
+ MissingServletRequestParameterException.class})
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ApiError handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+ String message = e.getBindingResult().getFieldErrors().stream()
+ .map(fe -> String.format("Field: %s. Error: %s. Value: %s",
+ fe.getField(),
+ fe.getDefaultMessage(),
+ fe.getRejectedValue() != null ? fe.getRejectedValue().toString() : "null"))
+ .collect(Collectors.joining("; "));
+
+ return new ApiError(
+ Collections.emptyList(),
+ message,
+ "Incorrectly made request.",
+ "BAD_REQUEST",
+ LocalDateTime.now()
+ );
+ }
+
+ @ExceptionHandler(ValidationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ApiError handleValidationException(ValidationException e) {
+ return new ApiError(
+ Collections.emptyList(),
+ e.getMessage(),
+ "Incorrectly made request.",
+ "BAD_REQUEST",
+ LocalDateTime.now()
+ );
+ }
+
+ @ExceptionHandler({ConflictException.class})
+ @ResponseStatus(HttpStatus.CONFLICT)
+ public ApiError handleConflictException(ConflictException e) {
+ return new ApiError(
+ Collections.emptyList(),
+ e.getMessage(),
+ "Integrity constraint has been violated.",
+ "CONFLICT",
+ LocalDateTime.now()
+ );
+ }
+
+ @ExceptionHandler({ConstraintViolationException.class})
+ @ResponseStatus(HttpStatus.CONFLICT)
+ public ApiError handleConstraintViolationException(ConstraintViolationException e) {
+ return new ApiError(
+ Collections.emptyList(),
+ "В базе данных нарушено ограничение целостности.",
+ "Integrity constraint has been violated.",
+ "CONFLICT",
+ LocalDateTime.now()
+ );
+ }
+
+ @ExceptionHandler(Throwable.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public ApiError handleThrowable(Throwable e) {
+ log.error("Неожиданная ошибка: ", e);
+ return new ApiError(
+ Collections.emptyList(),
+ "Произошла непредвиденная ошибка.",
+ "Internal server error.",
+ "INTERNAL_SERVER_ERROR",
+ LocalDateTime.now()
+ );
+ }
+}
diff --git a/main/src/main/java/ru/practicum/event/controller/EventAdminController.java b/main/src/main/java/ru/practicum/event/controller/EventAdminController.java
new file mode 100644
index 0000000..56ee807
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/controller/EventAdminController.java
@@ -0,0 +1,44 @@
+package ru.practicum.event.controller;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.UpdateEventAdminRequest;
+import ru.practicum.event.service.EventService;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/admin/events")
+public class EventAdminController {
+ private final EventService eventService;
+
+ @GetMapping
+ public List getAdminEvents(
+ @RequestParam(required = false) List users,
+ @RequestParam(required = false) List states,
+ @RequestParam(required = false) List categories,
+ @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeStart,
+ @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeEnd,
+ @RequestParam(defaultValue = "0") Integer from,
+ @RequestParam(defaultValue = "10") Integer size
+ ) {
+ log.info("GET /admin/events?users={}&states={}&categories={}&rangeStart={}&rangeEnd={}&from={}&size={}",
+ users, states, categories, rangeStart, rangeEnd, from, size);
+
+ return eventService.getAdminEvents(users, states, categories, rangeStart, rangeEnd, from, size);
+ }
+
+ @PatchMapping("/{eventId}")
+ public EventFullDto updateEventByAdmin(@PathVariable Long eventId,
+ @RequestBody @Valid UpdateEventAdminRequest updateEventAdminRequest) {
+ log.info("GET /admin/events/{}", eventId);
+ return eventService.updateEventByAdmin(eventId, updateEventAdminRequest);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java b/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java
new file mode 100644
index 0000000..08487b2
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java
@@ -0,0 +1,68 @@
+package ru.practicum.event.controller;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.PositiveOrZero;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.event.dto.*;
+import ru.practicum.event.service.EventService;
+import ru.practicum.request.dto.ParticipationRequestDto;
+
+import java.util.List;
+
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/users/{userId}/events")
+public class EventPrivateController {
+ private final EventService eventService;
+
+ @GetMapping
+ public List getEventsByUserId(@PathVariable("userId") Long userId,
+ @RequestParam(defaultValue = "0") @PositiveOrZero Integer from,
+ @RequestParam(defaultValue = "10") @Positive Integer size) {
+ log.info("GET /users/{}/events?from={}&size={}", userId, from, size);
+ return eventService.getEventsByUserId(userId, from, size);
+ }
+
+ @ResponseStatus(HttpStatus.CREATED)
+ @PostMapping
+ public EventFullDto createEvent(@PathVariable("userId") Long userId,
+ @Valid @RequestBody NewEventDto newEventDto) {
+ log.info("POST /users/{}/events", userId);
+ return eventService.createEvent(userId, newEventDto);
+ }
+
+ @GetMapping("/{eventId}")
+ public EventFullDto getEventByUserIdAndEventId(@PathVariable("userId") Long userId,
+ @PathVariable("eventId") Long eventId) {
+ log.info("GET /users/{}/events/{}", userId, eventId);
+ return eventService.getEventByUserIdAndEventId(userId, eventId);
+ }
+
+ @PatchMapping("/{eventId}")
+ public EventFullDto updateEventByUserIdAndEventId(@PathVariable("userId") Long userId,
+ @PathVariable("eventId") Long eventId,
+ @RequestBody @Valid UpdateEventUserRequest updateEventUserRequest) {
+ log.info("PATCH /users/{}/events/{}", userId, eventId);
+ return eventService.updateEventByUserIdAndEventId(userId, eventId, updateEventUserRequest);
+ }
+
+ @GetMapping("/{eventId}/requests")
+ public List getEventRequestsByUserIdAndEventId(@PathVariable(value = "userId") Long userId,
+ @PathVariable(value = "eventId") Long eventId) {
+ log.info("GET /users/{}/events/{}/requests", userId, eventId);
+ return eventService.getEventRequestsByUserIdAndEventId(userId, eventId);
+ }
+
+ @PatchMapping("/{eventId}/requests")
+ public EventRequestStatusUpdateResult changeStatus(@PathVariable final Long userId,
+ @PathVariable final Long eventId,
+ @RequestBody @Valid final EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest) {
+ log.info("PATCH /users/{}/events/{}/requests", userId, eventId);
+ return eventService.changeStatus(userId, eventId, eventRequestStatusUpdateRequest);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/event/controller/EventPublicController.java b/main/src/main/java/ru/practicum/event/controller/EventPublicController.java
new file mode 100644
index 0000000..8c0718d
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/controller/EventPublicController.java
@@ -0,0 +1,45 @@
+package ru.practicum.event.controller;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.EventShortDto;
+import ru.practicum.event.service.EventService;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/events")
+public class EventPublicController {
+ private final EventService eventService;
+
+ @GetMapping
+ public List getEvents(
+ @RequestParam(required = false) String text,
+ @RequestParam(required = false) List categories,
+ @RequestParam(required = false) Boolean paid,
+ @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeStart,
+ @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeEnd,
+ @RequestParam(defaultValue = "false") Boolean onlyAvailable,
+ @RequestParam(required = false) String sort,
+ @RequestParam(defaultValue = "0") Integer from,
+ @RequestParam(defaultValue = "10") Integer size,
+ HttpServletRequest request) {
+ log.info("GET /events?text={}&categories={}&paid={}&rangeStart={}&rangeEnd={}&onlyAvailable={}&sort={}&from={}&size={}",
+ text, categories, paid, rangeStart, rangeEnd, onlyAvailable, sort, from, size);
+
+ return eventService.getEvents(text, categories, paid, rangeStart, rangeEnd, onlyAvailable, sort, from, size, request);
+ }
+
+ @GetMapping("/{id}")
+ public EventFullDto getEventById(@PathVariable Long id, HttpServletRequest request) {
+ log.info("GET /events/{}", id);
+ return eventService.getEventById(id, request);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/event/dto/EventFullDto.java b/main/src/main/java/ru/practicum/event/dto/EventFullDto.java
new file mode 100644
index 0000000..32dda75
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/dto/EventFullDto.java
@@ -0,0 +1,54 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.event.state.EventState;
+import ru.practicum.location.dto.LocationDto;
+import ru.practicum.user.dto.UserShortDto;
+
+import java.time.LocalDateTime;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class EventFullDto {
+ private Long id;
+
+ private String annotation;
+
+ private CategoryDto category;
+
+ private Long confirmedRequests;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime createdOn;
+
+ private String description;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime eventDate;
+
+ private UserShortDto initiator;
+
+ private LocationDto location;
+
+ private Boolean paid;
+
+ private Integer participantLimit;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime publishedOn;
+
+ private Boolean requestModeration;
+
+ private EventState state;
+
+ private String title;
+
+ private Long views;
+}
diff --git a/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java
new file mode 100644
index 0000000..b81139a
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java
@@ -0,0 +1,21 @@
+package ru.practicum.event.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.request.status.RequestStatus;
+
+import java.util.List;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class EventRequestStatusUpdateRequest {
+ @NotNull(message = "Идентификаторы запросов обязательны")
+ List requestIds;
+ @NotNull(message = "Статус обязателен")
+ RequestStatus status;
+}
diff --git a/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateResult.java b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateResult.java
new file mode 100644
index 0000000..9168bd1
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateResult.java
@@ -0,0 +1,18 @@
+package ru.practicum.event.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.request.dto.ParticipationRequestDto;
+
+import java.util.List;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class EventRequestStatusUpdateResult {
+ List confirmedRequests;
+ List rejectedRequests;
+}
diff --git a/main/src/main/java/ru/practicum/event/dto/EventShortDto.java b/main/src/main/java/ru/practicum/event/dto/EventShortDto.java
new file mode 100644
index 0000000..1efd876
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/dto/EventShortDto.java
@@ -0,0 +1,36 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.user.dto.UserShortDto;
+
+import java.time.LocalDateTime;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class EventShortDto {
+ private Long id;
+
+ private String annotation;
+
+ private CategoryDto category;
+
+ private Long confirmedRequests;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime eventDate;
+
+ private UserShortDto initiator;
+
+ private Boolean paid;
+
+ private String title;
+
+ private Long views;
+}
diff --git a/main/src/main/java/ru/practicum/event/dto/NewEventDto.java b/main/src/main/java/ru/practicum/event/dto/NewEventDto.java
new file mode 100644
index 0000000..ca11e49
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/dto/NewEventDto.java
@@ -0,0 +1,49 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.location.dto.LocationDto;
+
+import java.time.LocalDateTime;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class NewEventDto {
+ @NotBlank(message = "Аннотация не может быть пустой")
+ @Size(min = 20, max = 2000, message = "Поле Аннотация должно быть в границах от {min} до {max} символов")
+ private String annotation;
+
+ @NotNull(message = "Категория обязательна")
+ private Long category;
+
+ @NotBlank(message = "Описание не может быть пустым")
+ @Size(min = 20, max = 7000, message = "Поле Описание должно быть в границах от {min} до {max} символов")
+ private String description;
+
+ @NotNull(message = "Дата события обязательна")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime eventDate;
+
+ @NotNull(message = "Локация обязательна")
+ private LocationDto location;
+
+ private Boolean paid;
+
+ @Min(value = 0, message = "Лимит участников не может быть отрицательным")
+ private Integer participantLimit;
+
+ private Boolean requestModeration;
+
+ @NotBlank(message = "Заголовок не может быть пустым")
+ @Size(min = 3, max = 120, message = "Поле Заголовок должно быть в границах от {min} до {max} символов")
+ private String title;
+}
diff --git a/main/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java b/main/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java
new file mode 100644
index 0000000..42308f5
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java
@@ -0,0 +1,42 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.event.state.EventStateActionSolution;
+import ru.practicum.location.dto.LocationDto;
+
+import java.time.LocalDateTime;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class UpdateEventAdminRequest {
+ @Size(min = 20, max = 2000, message = "Поле Аннотация должно быть в границах от {min} до {max} символов")
+ private String annotation;
+
+ private Long category;
+
+ @Size(min = 20, max = 7000, message = "Поле Описание должно быть в границах от {min} до {max} символов")
+ private String description;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime eventDate;
+
+ private LocationDto location;
+
+ private Boolean paid;
+
+ private Integer participantLimit;
+
+ private Boolean requestModeration;
+
+ private EventStateActionSolution stateAction;
+
+ @Size(min = 3, max = 120, message = "Поле Заголовок должно быть в границах от {min} до {max} символов")
+ private String title;
+}
diff --git a/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java b/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java
new file mode 100644
index 0000000..3d25dd5
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java
@@ -0,0 +1,44 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.event.state.EventStateAction;
+import ru.practicum.location.dto.LocationDto;
+
+import java.time.LocalDateTime;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class UpdateEventUserRequest {
+ @Size(min = 20, max = 2000, message = "Поле Аннотация должно быть в границах от {min} до {max} символов")
+ private String annotation;
+
+ private Long category;
+
+ @Size(min = 20, max = 7000, message = "Поле Описание должно быть в границах от {min} до {max} символов")
+ private String description;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime eventDate;
+
+ private LocationDto location;
+
+ private Boolean paid;
+
+ @Min(value = 0, message = "Лимит участников не может быть отрицательным")
+ private Integer participantLimit;
+
+ private Boolean requestModeration;
+
+ private EventStateAction stateAction;
+
+ @Size(min = 3, max = 120, message = "Поле Заголовок должно быть в границах от {min} до {max} символов")
+ private String title;
+}
diff --git a/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java
new file mode 100644
index 0000000..a482770
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java
@@ -0,0 +1,73 @@
+package ru.practicum.event.dto.mapper;
+
+import lombok.experimental.UtilityClass;
+import ru.practicum.category.dto.mapper.CategoryMapper;
+import ru.practicum.category.model.Category;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.EventShortDto;
+import ru.practicum.event.dto.NewEventDto;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.state.EventState;
+import ru.practicum.location.dto.mapper.LocationMapper;
+import ru.practicum.user.dto.mapper.UserMapper;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+
+@UtilityClass
+public class EventMapper {
+ public static EventFullDto toEventFullDto(Event event) {
+ return new EventFullDto(
+ event.getId(),
+ event.getAnnotation(),
+ CategoryMapper.toCategoryDto(event.getCategory()),
+ event.getConfirmedRequests(),
+ event.getCreatedOn(),
+ event.getDescription(),
+ event.getEventDate(),
+ UserMapper.toUserShortDto(event.getInitiator()),
+ LocationMapper.toLocationDto(event.getLocation()),
+ event.getPaid(),
+ event.getParticipantLimit(),
+ event.getPublishedOn(),
+ event.getRequestModeration(),
+ event.getState(),
+ event.getTitle(),
+ null
+ );
+ }
+
+ public static Event toEvent(NewEventDto newEventDto, Category category, User initiator) {
+ return new Event(
+ null,
+ newEventDto.getAnnotation(),
+ category,
+ 0L,
+ LocalDateTime.now(),
+ newEventDto.getDescription(),
+ newEventDto.getEventDate(),
+ initiator,
+ LocationMapper.toLocation(newEventDto.getLocation()),
+ newEventDto.getPaid(),
+ newEventDto.getParticipantLimit(),
+ null,
+ newEventDto.getRequestModeration(),
+ EventState.PENDING,
+ newEventDto.getTitle()
+ );
+ }
+
+ public static EventShortDto toEventShortDto(Event event) {
+ return new EventShortDto(
+ event.getId(),
+ event.getAnnotation(),
+ CategoryMapper.toCategoryDto(event.getCategory()),
+ event.getConfirmedRequests(),
+ event.getEventDate(),
+ UserMapper.toUserShortDto(event.getInitiator()),
+ event.getPaid(),
+ event.getTitle(),
+ null
+ );
+ }
+}
diff --git a/main/src/main/java/ru/practicum/event/model/Event.java b/main/src/main/java/ru/practicum/event/model/Event.java
new file mode 100644
index 0000000..9507b2c
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/model/Event.java
@@ -0,0 +1,84 @@
+package ru.practicum.event.model;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.category.model.Category;
+import ru.practicum.event.state.EventState;
+import ru.practicum.location.model.Location;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+@Table(name = "events")
+@Entity
+public class Event {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, length = 2000)
+ private String annotation;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "category_id")
+ private Category category;
+
+ @Column(name = "confirmed_requests")
+ private Long confirmedRequests;
+
+ @Column(name = "created_on", nullable = false)
+ private LocalDateTime createdOn;
+
+ @Column(length = 7000, nullable = false)
+ private String description;
+
+ @Column(name = "event_date", nullable = false)
+ private LocalDateTime eventDate;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "initiator_id")
+ private User initiator;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "location_id")
+ private Location location;
+
+ @Column(nullable = false)
+ private Boolean paid = false;
+
+ @Column(name = "participant_limit", nullable = false)
+ private Integer participantLimit = 0;
+
+ @Column(name = "published_on")
+ private LocalDateTime publishedOn;
+
+ @Column(name = "request_moderation", nullable = false)
+ private Boolean requestModeration = true;
+
+ @Enumerated(EnumType.STRING)
+ @Column(length = 50, nullable = false)
+ private EventState state;
+
+ @Column(nullable = false, length = 120)
+ private String title;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Event event)) return false;
+ return Objects.equals(id, event.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/event/repository/EventRepository.java b/main/src/main/java/ru/practicum/event/repository/EventRepository.java
new file mode 100644
index 0000000..faae129
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/repository/EventRepository.java
@@ -0,0 +1,20 @@
+package ru.practicum.event.repository;
+
+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 ru.practicum.event.state.EventState;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface EventRepository extends JpaRepository, JpaSpecificationExecutor {
+ List findByInitiatorId(Long userId, Pageable pageable);
+
+ Optional findByInitiatorIdAndId(Long userId, Long eventId);
+
+ Optional findByIdAndState(Long id, EventState eventState);
+
+ Boolean existsByCategoryId(Long categoryId);
+}
diff --git a/main/src/main/java/ru/practicum/event/service/EventService.java b/main/src/main/java/ru/practicum/event/service/EventService.java
new file mode 100644
index 0000000..ece6bb7
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/service/EventService.java
@@ -0,0 +1,45 @@
+package ru.practicum.event.service;
+
+import jakarta.servlet.http.HttpServletRequest;
+import ru.practicum.event.dto.*;
+import ru.practicum.request.dto.ParticipationRequestDto;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public interface EventService {
+ EventFullDto createEvent(Long userId, NewEventDto newEventDto);
+
+ List getEventsByUserId(Long userId, Integer from, Integer size);
+
+ EventFullDto getEventByUserIdAndEventId(Long userId, Long eventId);
+
+ EventFullDto updateEventByUserIdAndEventId(Long userId, Long eventId, UpdateEventUserRequest updateEventUserRequest);
+
+ List getEventRequestsByUserIdAndEventId(Long userId, Long eventId);
+
+ EventRequestStatusUpdateResult changeStatus(final Long userId,
+ final Long eventId,
+ final EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest);
+
+ List getEvents(String text, List categories, Boolean paid, LocalDateTime rangeStart,
+ LocalDateTime rangeEnd,
+ Boolean onlyAvailable,
+ String sort,
+ Integer from,
+ Integer size,
+ HttpServletRequest request);
+
+ EventFullDto getEventById(Long id, HttpServletRequest request);
+
+ List getAdminEvents(List users,
+ List states,
+ List categories,
+ LocalDateTime rangeStart,
+ LocalDateTime rangeEnd,
+ Integer from,
+ Integer size);
+
+ EventFullDto updateEventByAdmin(Long eventId,
+ UpdateEventAdminRequest updateEventAdminRequest);
+}
diff --git a/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java b/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java
new file mode 100644
index 0000000..fde6046
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java
@@ -0,0 +1,428 @@
+package ru.practicum.event.service;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.StatsClient;
+import ru.practicum.StatsRequestDto;
+import ru.practicum.StatsResponseDto;
+import ru.practicum.StatsViewRequestDto;
+import ru.practicum.category.model.Category;
+import ru.practicum.category.repository.CategoryRepository;
+import ru.practicum.event.dto.*;
+import ru.practicum.event.dto.mapper.EventMapper;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.repository.EventRepository;
+import ru.practicum.event.specification.EventSpecifications;
+import ru.practicum.event.state.EventState;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+import ru.practicum.exception.ValidationException;
+import ru.practicum.location.dto.mapper.LocationMapper;
+import ru.practicum.location.model.Location;
+import ru.practicum.location.repository.LocationRepository;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.dto.mapper.RequestMapper;
+import ru.practicum.request.model.Request;
+import ru.practicum.request.repository.RequestRepository;
+import ru.practicum.request.status.RequestStatus;
+import ru.practicum.user.model.User;
+import ru.practicum.user.repository.UserRepository;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Transactional(readOnly = true)
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class EventServiceImpl implements EventService {
+ private final EventRepository eventRepository;
+ private final UserRepository userRepository;
+ private final CategoryRepository categoryRepository;
+ private final RequestRepository requestRepository;
+ private final LocationRepository locationRepository;
+ private final StatsClient statsClient;
+
+
+ @Transactional
+ @Override
+ public EventFullDto createEvent(Long userId, NewEventDto newEventDto) {
+ User initiator = findUser(userId);
+ Category category = findCategory(newEventDto.getCategory());
+ validateEventDate(newEventDto.getEventDate());
+
+ Event event = EventMapper.toEvent(newEventDto, category, initiator);
+
+ if (event.getPaid() == null) {
+ event.setPaid(false);
+ }
+ if (event.getParticipantLimit() == null) {
+ event.setParticipantLimit(0);
+ }
+ if (event.getRequestModeration() == null) {
+ event.setRequestModeration(true);
+ }
+ if (event.getLocation() != null) {
+ Location savedLocation = locationRepository.save(event.getLocation());
+ event.setLocation(savedLocation);
+ }
+
+ return EventMapper.toEventFullDto(eventRepository.save(event));
+ }
+
+ @Override
+ public List getEventsByUserId(Long userId, Integer from, Integer size) {
+ findUser(userId);
+
+ Pageable pageable = PageRequest.of(from / size, size, Sort.by("eventDate").ascending());
+
+ return eventRepository.findByInitiatorId(userId, pageable)
+ .stream()
+ .map(EventMapper::toEventShortDto)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public EventFullDto getEventByUserIdAndEventId(Long userId, Long eventId) {
+ findUser(userId);
+ Event event = findEvent(userId, eventId);
+
+ return EventMapper.toEventFullDto(event);
+ }
+
+ @Transactional
+ @Override
+ public EventFullDto updateEventByUserIdAndEventId(Long userId,
+ Long eventId,
+ UpdateEventUserRequest updateEventUserRequest) {
+ findUser(userId);
+ Event event = findEvent(userId, eventId);
+
+ if (!(event.getState().equals(EventState.PENDING) || event.getState().equals(EventState.CANCELED))) {
+ log.warn("Изменять можно только события в состоянии ожидания модерации или отмененные. Текущее состояние: {}", event.getState());
+ throw new ConflictException("Изменять можно только события в состоянии ожидания модерации или отмененные");
+ }
+
+ if (updateEventUserRequest.getEventDate() != null) {
+ validateEventDate(updateEventUserRequest.getEventDate());
+ event.setEventDate(updateEventUserRequest.getEventDate());
+ }
+
+ if (updateEventUserRequest.getAnnotation() != null) {
+ event.setAnnotation(updateEventUserRequest.getAnnotation());
+ }
+ if (updateEventUserRequest.getCategory() != null) {
+ Category category = findCategory(updateEventUserRequest.getCategory());
+ event.setCategory(category);
+ }
+ if (updateEventUserRequest.getDescription() != null) {
+ event.setDescription(updateEventUserRequest.getDescription());
+ }
+ if (updateEventUserRequest.getPaid() != null) {
+ event.setPaid(updateEventUserRequest.getPaid());
+ }
+ if (updateEventUserRequest.getParticipantLimit() != null) {
+ event.setParticipantLimit(updateEventUserRequest.getParticipantLimit());
+ }
+ if (updateEventUserRequest.getTitle() != null) {
+ event.setTitle(updateEventUserRequest.getTitle());
+ }
+
+ if (updateEventUserRequest.getStateAction() != null) {
+ switch (updateEventUserRequest.getStateAction()) {
+ case SEND_TO_REVIEW -> event.setState(EventState.PENDING);
+ case CANCEL_REVIEW -> event.setState(EventState.CANCELED);
+ default -> throw new ConflictException("Недопустимое состояние события для обновления");
+ }
+ }
+
+ return EventMapper.toEventFullDto(eventRepository.save(event));
+ }
+
+ @Override
+ public List getEventRequestsByUserIdAndEventId(Long userId, Long eventId) {
+ findEvent(userId, eventId);
+
+ List requests = requestRepository.findAllByEventId(eventId);
+
+ return requests.stream()
+ .map(RequestMapper::toParticipationRequestDto)
+ .toList();
+ }
+
+ @Transactional
+ @Override
+ public EventRequestStatusUpdateResult changeStatus(Long userId, Long eventId, EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest) {
+ Event event = findEvent(userId, eventId);
+
+ if (event.getParticipantLimit() == 0 || Boolean.FALSE.equals(event.getRequestModeration())) {
+ List requests = requestRepository.findAllByIdIn(eventRequestStatusUpdateRequest.getRequestIds());
+ List confirmed = requests.stream()
+ .map(RequestMapper::toParticipationRequestDto)
+ .toList();
+ return new EventRequestStatusUpdateResult(confirmed, Collections.emptyList());
+ }
+
+ long confirmedCount = requestRepository.countByEventIdAndStatus(eventId, RequestStatus.CONFIRMED);
+
+ List requestsToUpdate = requestRepository.findAllByIdIn(eventRequestStatusUpdateRequest.getRequestIds());
+
+ List confirmedRequests = new ArrayList<>();
+ List rejectedRequests = new ArrayList<>();
+
+ for (Request r : requestsToUpdate) {
+ if (!r.getStatus().equals(RequestStatus.PENDING)) {
+ throw new ConflictException("Запрос должен иметь статус PENDING");
+ }
+
+ if (eventRequestStatusUpdateRequest.getStatus().equals(RequestStatus.CONFIRMED)) {
+ if (confirmedCount >= event.getParticipantLimit()) {
+ throw new ConflictException("Лимит участников достигнут");
+ }
+ r.setStatus(RequestStatus.CONFIRMED);
+ confirmedCount++;
+ confirmedRequests.add(RequestMapper.toParticipationRequestDto(r));
+ } else if (eventRequestStatusUpdateRequest.getStatus().equals(RequestStatus.REJECTED)) {
+ r.setStatus(RequestStatus.REJECTED);
+ rejectedRequests.add(RequestMapper.toParticipationRequestDto(r));
+ } else {
+ throw new ConflictException("Неверный статус для обновления");
+ }
+
+ if (confirmedCount >= event.getParticipantLimit()) {
+ List pendingRequests = requestRepository.findAllByEventIdAndStatus(eventId, RequestStatus.PENDING);
+ for (Request pr : pendingRequests) {
+ pr.setStatus(RequestStatus.REJECTED);
+ rejectedRequests.add(RequestMapper.toParticipationRequestDto(pr));
+ }
+ break;
+ }
+ }
+ requestRepository.saveAll(requestsToUpdate);
+
+ return new EventRequestStatusUpdateResult(confirmedRequests, rejectedRequests);
+ }
+
+ @Override
+ public List getEvents(String text, List categories, Boolean paid, LocalDateTime rangeStart,
+ LocalDateTime rangeEnd,
+ Boolean onlyAvailable,
+ String sort,
+ Integer from,
+ Integer size,
+ HttpServletRequest request) {
+ if (rangeStart != null && rangeEnd != null && rangeEnd.isBefore(rangeStart)) {
+ log.warn("rangeEnd не может быть меньше rangeStart");
+ throw new ValidationException("rangeEnd не может быть меньше rangeStart");
+ }
+
+ Specification spec = Specification.where(EventSpecifications.published())
+ .and(EventSpecifications.textContains(text))
+ .and(EventSpecifications.categoryIn(categories))
+ .and(EventSpecifications.paidIs(paid))
+ .and(EventSpecifications.dateBetween(rangeStart, rangeEnd))
+ .and(EventSpecifications.onlyAvailable(onlyAvailable));
+
+ Pageable pageable = PageRequest.of(from / size, size, sortBy(sort));
+
+ List events = eventRepository.findAll(spec, pageable).getContent();
+
+ statsClient.createHit(new StatsRequestDto(
+ "ewm",
+ request.getRequestURI(),
+ request.getRemoteAddr(),
+ LocalDateTime.now()
+ ));
+
+ Map eventUriAndIdMap = events.stream()
+ .map(Event::getId)
+ .collect(Collectors.toMap(id -> "/events/" + id, Function.identity()));
+
+ List stats = statsClient.getStats(
+ StatsViewRequestDto.builder()
+ .uris(new ArrayList<>(eventUriAndIdMap.keySet()))
+ .unique(true)
+ .build()
+ );
+
+ Map views = stats.stream()
+ .collect(Collectors.toMap(StatsResponseDto::getUri,
+ StatsResponseDto::getHits
+ ));
+
+ return events.stream()
+ .map(event -> {
+ EventShortDto eventShortDto = EventMapper.toEventShortDto(event);
+ eventShortDto.setViews(views.getOrDefault("/events/" + event.getId(), 0L));
+ return eventShortDto;
+ })
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public EventFullDto getEventById(Long id, HttpServletRequest request) {
+ Event event = eventRepository.findByIdAndState(id, EventState.PUBLISHED)
+ .orElseThrow(() -> new NotFoundException("Опубликованное событие с id:" + id + " не найдено"));
+
+ statsClient.createHit(new StatsRequestDto(
+ "ewm",
+ request.getRequestURI(),
+ request.getRemoteAddr(),
+ LocalDateTime.now()
+ ));
+
+ Long views = statsClient.getStats(
+ StatsViewRequestDto.builder()
+ .uris(List.of("/events/" + id))
+ .start(event.getPublishedOn())
+ .end(LocalDateTime.now())
+ .unique(true)
+ .build()
+
+ ).stream()
+ .findAny()
+ .map(StatsResponseDto::getHits)
+ .orElse(0L);
+
+ EventFullDto eventFullDto = EventMapper.toEventFullDto(event);
+ eventFullDto.setViews(views);
+
+ return eventFullDto;
+ }
+
+ @Override
+ public List getAdminEvents(List users, List states,
+ List categories,
+ LocalDateTime rangeStart,
+ LocalDateTime rangeEnd,
+ Integer from,
+ Integer size) {
+
+ Specification spec = Specification.where(EventSpecifications.userIn(users))
+ .and(EventSpecifications.stateIn(states))
+ .and(EventSpecifications.categoryIn(categories))
+ .and(EventSpecifications.dateBetween(rangeStart, rangeEnd));
+
+ Pageable pageable = PageRequest.of(from / size, size, Sort.by(Sort.Direction.ASC, "eventDate"));
+
+ List events = eventRepository.findAll(spec, pageable).getContent();
+
+ events.forEach(e -> {
+ long confirmed = requestRepository.countByEventIdAndStatus(e.getId(), RequestStatus.CONFIRMED);
+ e.setConfirmedRequests(confirmed);
+ });
+
+
+ return events.stream()
+ .map(e -> {
+ EventFullDto dto = EventMapper.toEventFullDto(e);
+ Long confirmed = requestRepository.countByEventIdAndStatus(e.getId(), RequestStatus.CONFIRMED);
+ dto.setConfirmedRequests(confirmed);
+ return dto;
+ })
+ .collect(Collectors.toList());
+ }
+
+ @Transactional
+ @Override
+ public EventFullDto updateEventByAdmin(Long eventId, UpdateEventAdminRequest updateEventAdminRequest) {
+ Event event = eventRepository.findById(eventId)
+ .orElseThrow(() -> new NotFoundException("Событие с id:" + eventId + " не найдено"));
+
+ if (updateEventAdminRequest.getAnnotation() != null) {
+ event.setAnnotation(updateEventAdminRequest.getAnnotation());
+ }
+ if (updateEventAdminRequest.getDescription() != null) {
+ event.setDescription(updateEventAdminRequest.getDescription());
+ }
+ if (updateEventAdminRequest.getTitle() != null) {
+ event.setTitle(updateEventAdminRequest.getTitle());
+ }
+ if (updateEventAdminRequest.getCategory() != null) {
+ Category category = findCategory(updateEventAdminRequest.getCategory());
+ event.setCategory(category);
+ }
+ if (updateEventAdminRequest.getLocation() != null) {
+ Location location = locationRepository.save(LocationMapper.toLocation(updateEventAdminRequest.getLocation()));
+ event.setLocation(location);
+ }
+ if (updateEventAdminRequest.getEventDate() != null) {
+ if (updateEventAdminRequest.getEventDate().isBefore(LocalDateTime.now().plusHours(1))) {
+ throw new ValidationException("Дата события должна быть не позднее, чем через 1 час после публикации");
+ }
+ event.setEventDate(updateEventAdminRequest.getEventDate());
+ }
+ if (updateEventAdminRequest.getPaid() != null) {
+ event.setPaid(updateEventAdminRequest.getPaid());
+ }
+ if (updateEventAdminRequest.getParticipantLimit() != null) {
+ event.setParticipantLimit(updateEventAdminRequest.getParticipantLimit());
+ }
+ if (updateEventAdminRequest.getRequestModeration() != null) {
+ event.setRequestModeration(updateEventAdminRequest.getRequestModeration());
+ }
+
+ if (updateEventAdminRequest.getStateAction() != null) {
+ switch (updateEventAdminRequest.getStateAction()) {
+ case PUBLISH_EVENT:
+ if (event.getState() != EventState.PENDING) {
+ throw new ConflictException("Невозможно опубликовать событие, так как оно находится в неправильном состоянии: " + event.getState());
+ }
+ event.setState(EventState.PUBLISHED);
+ event.setPublishedOn(LocalDateTime.now());
+ break;
+ case REJECT_EVENT:
+ if (event.getState() == EventState.PUBLISHED) {
+ throw new ConflictException("Невозможно отклонить событие, так как оно уже опубликовано");
+ }
+ event.setState(EventState.CANCELED);
+ break;
+ }
+ }
+
+ return EventMapper.toEventFullDto(eventRepository.save(event));
+ }
+
+ private Sort sortBy(String sort) {
+ if ("views".equalsIgnoreCase(sort)) {
+ return Sort.by(Sort.Direction.DESC, "views");
+ }
+ return Sort.by(Sort.Direction.ASC, "eventDate");
+ }
+
+
+ private User findUser(Long id) {
+ return userRepository.findById(id)
+ .orElseThrow(() -> new NotFoundException("Пользователь с id: " + id + " не найден"));
+ }
+
+ private Category findCategory(Long id) {
+ return categoryRepository.findById(id)
+ .orElseThrow(() -> new NotFoundException("Категория с id: " + id + " не найдена"));
+ }
+
+ private Event findEvent(Long userId, Long eventId) {
+ return eventRepository.findByInitiatorIdAndId(userId, eventId)
+ .orElseThrow(() -> new NotFoundException("Событие текущего пользователя не найдено"));
+ }
+
+ private void validateEventDate(LocalDateTime date) {
+ LocalDateTime now = LocalDateTime.now();
+ if (date.isBefore(now.plusHours(2))) {
+ log.warn("Дата события должна быть не менее чем через 2 часа от текущего времени");
+ throw new ValidationException("Дата события должна быть не менее чем через 2 часа от текущего времени");
+ }
+ }
+}
diff --git a/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java b/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java
new file mode 100644
index 0000000..0b9b04e
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java
@@ -0,0 +1,76 @@
+package ru.practicum.event.specification;
+
+import org.springframework.data.jpa.domain.Specification;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.state.EventState;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class EventSpecifications {
+ public static Specification textContains(String text) {
+ return (root, query, cb) -> {
+ if (text == null || text.isBlank()) return null;
+ String pattern = "%" + text.toLowerCase() + "%";
+ return cb.or(
+ cb.like(cb.lower(root.get("annotation")), pattern),
+ cb.like(cb.lower(root.get("description")), pattern)
+ );
+ };
+ }
+
+ public static Specification categoryIn(List categories) {
+ return (root, query, cb) -> {
+ if (categories == null || categories.isEmpty()) return null;
+ return root.get("category").get("id").in(categories);
+ };
+ }
+
+ public static Specification paidIs(Boolean paid) {
+ return (root, query, cb) -> {
+ if (paid == null) return null;
+ return cb.equal(root.get("paid"), paid);
+ };
+ }
+
+ public static Specification dateBetween(LocalDateTime start, LocalDateTime end) {
+ LocalDateTime startDate = (start != null) ? start : LocalDateTime.now();
+
+ return (root, query, cb) -> {
+ if (end != null) {
+ return cb.between(root.get("eventDate"), startDate, end);
+ } else {
+ return cb.greaterThanOrEqualTo(root.get("eventDate"), startDate);
+ }
+ };
+ }
+
+ public static Specification onlyAvailable(Boolean onlyAvailable) {
+ return (root, query, cb) -> {
+ if (onlyAvailable == null || !onlyAvailable) return null;
+ return cb.greaterThan(root.get("participantLimit"), root.get("confirmedRequests"));
+ };
+ }
+
+ public static Specification published() {
+ return (root, query, cb) -> cb.equal(root.get("state"), EventState.PUBLISHED);
+ }
+
+ public static Specification userIn(List userIds) {
+ return (root, query, cb) -> {
+ if (userIds == null || userIds.isEmpty()) return null;
+ return root.get("initiator").get("id").in(userIds);
+ };
+ }
+
+ public static Specification stateIn(List states) {
+ return (root, query, cb) -> {
+ if (states == null || states.isEmpty()) return null;
+ List eventStates = states.stream()
+ .map(EventState::valueOf)
+ .collect(Collectors.toList());
+ return root.get("state").in(eventStates);
+ };
+ }
+}
diff --git a/main/src/main/java/ru/practicum/event/state/EventState.java b/main/src/main/java/ru/practicum/event/state/EventState.java
new file mode 100644
index 0000000..7849491
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/state/EventState.java
@@ -0,0 +1,7 @@
+package ru.practicum.event.state;
+
+public enum EventState {
+ PENDING,
+ PUBLISHED,
+ CANCELED
+}
diff --git a/main/src/main/java/ru/practicum/event/state/EventStateAction.java b/main/src/main/java/ru/practicum/event/state/EventStateAction.java
new file mode 100644
index 0000000..905addd
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/state/EventStateAction.java
@@ -0,0 +1,6 @@
+package ru.practicum.event.state;
+
+public enum EventStateAction {
+ SEND_TO_REVIEW,
+ CANCEL_REVIEW
+}
diff --git a/main/src/main/java/ru/practicum/event/state/EventStateActionSolution.java b/main/src/main/java/ru/practicum/event/state/EventStateActionSolution.java
new file mode 100644
index 0000000..8740cc3
--- /dev/null
+++ b/main/src/main/java/ru/practicum/event/state/EventStateActionSolution.java
@@ -0,0 +1,6 @@
+package ru.practicum.event.state;
+
+public enum EventStateActionSolution {
+ PUBLISH_EVENT,
+ REJECT_EVENT
+}
diff --git a/main/src/main/java/ru/practicum/exception/ConflictException.java b/main/src/main/java/ru/practicum/exception/ConflictException.java
new file mode 100644
index 0000000..e2fddf1
--- /dev/null
+++ b/main/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/src/main/java/ru/practicum/exception/NotFoundException.java b/main/src/main/java/ru/practicum/exception/NotFoundException.java
new file mode 100644
index 0000000..628c788
--- /dev/null
+++ b/main/src/main/java/ru/practicum/exception/NotFoundException.java
@@ -0,0 +1,7 @@
+package ru.practicum.exception;
+
+public class NotFoundException extends RuntimeException {
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/exception/ValidationException.java b/main/src/main/java/ru/practicum/exception/ValidationException.java
new file mode 100644
index 0000000..d751d21
--- /dev/null
+++ b/main/src/main/java/ru/practicum/exception/ValidationException.java
@@ -0,0 +1,7 @@
+package ru.practicum.exception;
+
+public class ValidationException extends RuntimeException {
+ public ValidationException(String message) {
+ super(message);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/location/dto/LocationDto.java b/main/src/main/java/ru/practicum/location/dto/LocationDto.java
new file mode 100644
index 0000000..2547c6c
--- /dev/null
+++ b/main/src/main/java/ru/practicum/location/dto/LocationDto.java
@@ -0,0 +1,25 @@
+package ru.practicum.location.dto;
+
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class LocationDto {
+ @Min(-90)
+ @Max(90)
+ @NotNull
+ private Float lat;
+
+ @Min(-180)
+ @Max(180)
+ @NotNull
+ private Float lon;
+}
diff --git a/main/src/main/java/ru/practicum/location/dto/mapper/LocationMapper.java b/main/src/main/java/ru/practicum/location/dto/mapper/LocationMapper.java
new file mode 100644
index 0000000..996437e
--- /dev/null
+++ b/main/src/main/java/ru/practicum/location/dto/mapper/LocationMapper.java
@@ -0,0 +1,23 @@
+package ru.practicum.location.dto.mapper;
+
+import lombok.experimental.UtilityClass;
+import ru.practicum.location.dto.LocationDto;
+import ru.practicum.location.model.Location;
+
+@UtilityClass
+public class LocationMapper {
+ public static LocationDto toLocationDto(Location location) {
+ return new LocationDto(
+ location.getLat(),
+ location.getLon()
+ );
+ }
+
+ public static Location toLocation(LocationDto locationDto) {
+ return new Location(
+ null,
+ locationDto.getLat(),
+ locationDto.getLon()
+ );
+ }
+}
diff --git a/main/src/main/java/ru/practicum/location/model/Location.java b/main/src/main/java/ru/practicum/location/model/Location.java
new file mode 100644
index 0000000..779ea3d
--- /dev/null
+++ b/main/src/main/java/ru/practicum/location/model/Location.java
@@ -0,0 +1,39 @@
+package ru.practicum.location.model;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.Objects;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+@Table(name = "locations")
+@Entity
+public class Location {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false)
+ private Float lat;
+
+ @Column(nullable = false)
+ private Float lon;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Location location)) return false;
+ return Objects.equals(id, location.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/location/repository/LocationRepository.java b/main/src/main/java/ru/practicum/location/repository/LocationRepository.java
new file mode 100644
index 0000000..c183685
--- /dev/null
+++ b/main/src/main/java/ru/practicum/location/repository/LocationRepository.java
@@ -0,0 +1,7 @@
+package ru.practicum.location.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.practicum.location.model.Location;
+
+public interface LocationRepository extends JpaRepository {
+}
diff --git a/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java b/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java
new file mode 100644
index 0000000..cc3a2c2
--- /dev/null
+++ b/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java
@@ -0,0 +1,39 @@
+package ru.practicum.request.controller;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.service.RequestService;
+
+import java.util.List;
+
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/users/{userId}/requests")
+public class RequestPrivateController {
+ private final RequestService requestService;
+
+ @GetMapping
+ public List getRequestsByUserId(@PathVariable(value = "userId") Long userId) {
+ log.info("GET /users/{}/requests", userId);
+ return requestService.getRequestsByUserId(userId);
+ }
+
+ @ResponseStatus(HttpStatus.CREATED)
+ @PostMapping
+ public ParticipationRequestDto createRequest(@PathVariable(value = "userId") Long userId,
+ @RequestParam(name = "eventId") Long eventId) {
+ log.info("Запрос на участие в событии с id= {}. POST /users/{}/requests", eventId, userId);
+ return requestService.createRequest(userId, eventId);
+ }
+
+ @PatchMapping("/{requestId}/cancel")
+ public ParticipationRequestDto cancelRequest(@PathVariable(value = "userId") Long userId,
+ @PathVariable(value = "requestId") Long requestId) {
+ log.info("GET /users/{}/requests/{}/cancel", userId, requestId);
+ return requestService.cancelRequest(userId, requestId);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java b/main/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java
new file mode 100644
index 0000000..11e6670
--- /dev/null
+++ b/main/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java
@@ -0,0 +1,27 @@
+package ru.practicum.request.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.request.status.RequestStatus;
+
+import java.time.LocalDateTime;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class ParticipationRequestDto {
+ private Long id;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime created;
+
+ private Long event;
+
+ private Long requester;
+
+ private RequestStatus status;
+}
diff --git a/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java b/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java
new file mode 100644
index 0000000..f3c04fc
--- /dev/null
+++ b/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java
@@ -0,0 +1,18 @@
+package ru.practicum.request.dto.mapper;
+
+import lombok.experimental.UtilityClass;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.model.Request;
+
+@UtilityClass
+public class RequestMapper {
+ public static ParticipationRequestDto toParticipationRequestDto(Request request) {
+ return new ParticipationRequestDto(
+ request.getId(),
+ request.getCreated(),
+ request.getEvent().getId(),
+ request.getRequester().getId(),
+ request.getStatus()
+ );
+ }
+}
diff --git a/main/src/main/java/ru/practicum/request/model/Request.java b/main/src/main/java/ru/practicum/request/model/Request.java
new file mode 100644
index 0000000..67bdc0d
--- /dev/null
+++ b/main/src/main/java/ru/practicum/request/model/Request.java
@@ -0,0 +1,55 @@
+package ru.practicum.request.model;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import ru.practicum.event.model.Event;
+import ru.practicum.request.status.RequestStatus;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+@Entity
+@Table(
+ name = "requests",
+ uniqueConstraints = {@UniqueConstraint(columnNames = {"event_id", "requester_id"})}
+)
+public class Request {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "create_date", nullable = false)
+ private LocalDateTime created = LocalDateTime.now();
+
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "event_id", nullable = false)
+ private Event event;
+
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "requester_id", nullable = false)
+ private User requester;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private RequestStatus status = RequestStatus.PENDING;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Request request)) return false;
+ return Objects.equals(id, request.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/request/repository/RequestRepository.java b/main/src/main/java/ru/practicum/request/repository/RequestRepository.java
new file mode 100644
index 0000000..4c382dd
--- /dev/null
+++ b/main/src/main/java/ru/practicum/request/repository/RequestRepository.java
@@ -0,0 +1,21 @@
+package ru.practicum.request.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.practicum.request.model.Request;
+import ru.practicum.request.status.RequestStatus;
+
+import java.util.List;
+
+public interface RequestRepository extends JpaRepository {
+ Boolean existsByRequesterIdAndEventId(Long requesterId, Long eventId);
+
+ Long countByEventIdAndStatus(Long eventId, RequestStatus status);
+
+ List findAllByRequesterId(Long requesterId);
+
+ List findAllByEventId(Long eventId);
+
+ List findAllByIdIn(List ids);
+
+ List findAllByEventIdAndStatus(Long eventId, RequestStatus status);
+}
diff --git a/main/src/main/java/ru/practicum/request/service/RequestService.java b/main/src/main/java/ru/practicum/request/service/RequestService.java
new file mode 100644
index 0000000..f1dd929
--- /dev/null
+++ b/main/src/main/java/ru/practicum/request/service/RequestService.java
@@ -0,0 +1,13 @@
+package ru.practicum.request.service;
+
+import ru.practicum.request.dto.ParticipationRequestDto;
+
+import java.util.List;
+
+public interface RequestService {
+ ParticipationRequestDto createRequest(Long userId, Long eventId);
+
+ List getRequestsByUserId(Long userId);
+
+ ParticipationRequestDto cancelRequest(Long userId, Long requestId);
+}
diff --git a/main/src/main/java/ru/practicum/request/service/RequestServiceImpl.java b/main/src/main/java/ru/practicum/request/service/RequestServiceImpl.java
new file mode 100644
index 0000000..0a4faf7
--- /dev/null
+++ b/main/src/main/java/ru/practicum/request/service/RequestServiceImpl.java
@@ -0,0 +1,108 @@
+package ru.practicum.request.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.repository.EventRepository;
+import ru.practicum.event.state.EventState;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.dto.mapper.RequestMapper;
+import ru.practicum.request.model.Request;
+import ru.practicum.request.repository.RequestRepository;
+import ru.practicum.request.status.RequestStatus;
+import ru.practicum.user.model.User;
+import ru.practicum.user.repository.UserRepository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Transactional(readOnly = true)
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class RequestServiceImpl implements RequestService {
+ private final RequestRepository requestRepository;
+ private final UserRepository userRepository;
+ private final EventRepository eventRepository;
+
+ @Transactional
+ @Override
+ public ParticipationRequestDto createRequest(Long userId, Long eventId) {
+ User user = findUser(userId);
+ Event event = findEvent(eventId);
+
+ if (event.getInitiator().getId().equals(userId)) {
+ throw new ConflictException("Инициатор события не может добавить заявку на участие в своём событии");
+ }
+
+ if (!EventState.PUBLISHED.equals(event.getState())) {
+ throw new ConflictException("Нельзя участвовать в неопубликованном событии");
+ }
+
+ boolean exists = requestRepository.existsByRequesterIdAndEventId(userId, eventId);
+ if (exists) {
+ throw new ConflictException("Нельзя добавить повторный запрос на участие в событии");
+ }
+
+ long confirmedRequests = requestRepository.countByEventIdAndStatus(eventId, RequestStatus.CONFIRMED);
+ if (event.getParticipantLimit() != null && event.getParticipantLimit() > 0 &&
+ confirmedRequests >= event.getParticipantLimit()) {
+ throw new ConflictException("Достигнут лимит запросов на участие в событии");
+ }
+
+ Request request = new Request();
+ request.setEvent(event);
+ request.setRequester(user);
+ request.setCreated(LocalDateTime.now());
+
+ if (Boolean.FALSE.equals(event.getRequestModeration()) || event.getParticipantLimit() == 0) {
+ request.setStatus(RequestStatus.CONFIRMED);
+ } else {
+ request.setStatus(RequestStatus.PENDING);
+ }
+
+ return RequestMapper.toParticipationRequestDto(requestRepository.save(request));
+ }
+
+ @Override
+ public List getRequestsByUserId(Long userId) {
+ findUser(userId);
+
+ List requests = requestRepository.findAllByRequesterId(userId);
+
+ return requests.stream()
+ .map(RequestMapper::toParticipationRequestDto)
+ .toList();
+ }
+
+ @Transactional
+ @Override
+ public ParticipationRequestDto cancelRequest(Long userId, Long requestId) {
+ findUser(userId);
+
+ Request request = requestRepository.findById(requestId)
+ .orElseThrow(() -> new NotFoundException("Запрос с id:" + requestId + " не найден"));
+
+ if (!request.getRequester().getId().equals(userId)) {
+ throw new NotFoundException("Заявка с id:" + requestId + " недоступна для пользователя c id:" + userId);
+ }
+
+ request.setStatus(RequestStatus.CANCELED);
+
+ return RequestMapper.toParticipationRequestDto(requestRepository.save(request));
+ }
+
+ private User findUser(Long id) {
+ return userRepository.findById(id)
+ .orElseThrow(() -> new NotFoundException("Пользователь с id:" + id + " не найден"));
+ }
+
+ private Event findEvent(Long id) {
+ return eventRepository.findById(id)
+ .orElseThrow(() -> new NotFoundException("Событие с id:" + id + " не найдено"));
+ }
+}
diff --git a/main/src/main/java/ru/practicum/request/status/RequestStatus.java b/main/src/main/java/ru/practicum/request/status/RequestStatus.java
new file mode 100644
index 0000000..680575b
--- /dev/null
+++ b/main/src/main/java/ru/practicum/request/status/RequestStatus.java
@@ -0,0 +1,8 @@
+package ru.practicum.request.status;
+
+public enum RequestStatus {
+ PENDING,
+ CONFIRMED,
+ REJECTED,
+ CANCELED
+}
diff --git a/main/src/main/java/ru/practicum/user/controller/UserAdminController.java b/main/src/main/java/ru/practicum/user/controller/UserAdminController.java
new file mode 100644
index 0000000..9cce4cf
--- /dev/null
+++ b/main/src/main/java/ru/practicum/user/controller/UserAdminController.java
@@ -0,0 +1,44 @@
+package ru.practicum.user.controller;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.PositiveOrZero;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.user.dto.NewUserRequest;
+import ru.practicum.user.dto.UserDto;
+import ru.practicum.user.service.UserService;
+
+import java.util.List;
+
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(path = "/admin/users")
+public class UserAdminController {
+ private final UserService userService;
+
+ @GetMapping
+ public List getUsers(@RequestParam(required = false) List ids,
+ @RequestParam(defaultValue = "0") @PositiveOrZero Integer from,
+ @RequestParam(defaultValue = "10") @Positive Integer size) {
+ log.info("GET /admin/users");
+ return userService.getUsers(ids, from, size);
+ }
+
+ @ResponseStatus(HttpStatus.CREATED)
+ @PostMapping
+ public UserDto createUser(@RequestBody @Valid NewUserRequest newUserRequest) {
+ log.info("POST /admin/users");
+ return userService.createUser(newUserRequest);
+ }
+
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @DeleteMapping("/{userId}")
+ public void deleteUser(@PathVariable("userId") Long id) {
+ log.info("DELETE /admin/users/{}", id);
+ userService.deleteUser(id);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/user/dto/NewUserRequest.java b/main/src/main/java/ru/practicum/user/dto/NewUserRequest.java
new file mode 100644
index 0000000..aef7a3d
--- /dev/null
+++ b/main/src/main/java/ru/practicum/user/dto/NewUserRequest.java
@@ -0,0 +1,24 @@
+package ru.practicum.user.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class NewUserRequest {
+ @Email(message = "Email должен быть в корректном формате")
+ @NotBlank(message = "Email не может быть пустым")
+ @Size(min = 6, max = 254, message = "Email должен быть в границах от {min} до {max} символов")
+ private String email;
+
+ @NotBlank(message = "Имя пользователя не может быть пустым")
+ @Size(min = 2, max = 250, message = "Имя пользователя должно быть в границах от {min} до {max} символов")
+ private String name;
+}
diff --git a/main/src/main/java/ru/practicum/user/dto/UserDto.java b/main/src/main/java/ru/practicum/user/dto/UserDto.java
new file mode 100644
index 0000000..66728db
--- /dev/null
+++ b/main/src/main/java/ru/practicum/user/dto/UserDto.java
@@ -0,0 +1,16 @@
+package ru.practicum.user.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class UserDto {
+ private Long id;
+ private String email;
+ private String name;
+}
diff --git a/main/src/main/java/ru/practicum/user/dto/UserShortDto.java b/main/src/main/java/ru/practicum/user/dto/UserShortDto.java
new file mode 100644
index 0000000..09df516
--- /dev/null
+++ b/main/src/main/java/ru/practicum/user/dto/UserShortDto.java
@@ -0,0 +1,15 @@
+package ru.practicum.user.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+public class UserShortDto {
+ private Long id;
+ private String name;
+}
diff --git a/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java b/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java
new file mode 100644
index 0000000..e10d20f
--- /dev/null
+++ b/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java
@@ -0,0 +1,33 @@
+package ru.practicum.user.dto.mapper;
+
+import lombok.experimental.UtilityClass;
+import ru.practicum.user.dto.NewUserRequest;
+import ru.practicum.user.dto.UserDto;
+import ru.practicum.user.dto.UserShortDto;
+import ru.practicum.user.model.User;
+
+@UtilityClass
+public class UserMapper {
+ public static UserDto toUserDto(User user) {
+ return new UserDto(
+ user.getId(),
+ user.getEmail(),
+ user.getName()
+ );
+ }
+
+ public static User toUser(NewUserRequest newUserRequest) {
+ return new User(
+ null,
+ newUserRequest.getEmail(),
+ newUserRequest.getName()
+ );
+ }
+
+ public static UserShortDto toUserShortDto(User user) {
+ return new UserShortDto(
+ user.getId(),
+ user.getName()
+ );
+ }
+}
diff --git a/main/src/main/java/ru/practicum/user/model/User.java b/main/src/main/java/ru/practicum/user/model/User.java
new file mode 100644
index 0000000..9b43fa3
--- /dev/null
+++ b/main/src/main/java/ru/practicum/user/model/User.java
@@ -0,0 +1,39 @@
+package ru.practicum.user.model;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.Objects;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
+@Table(name = "users")
+@Entity
+public class User {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, unique = true, length = 254)
+ private String email;
+
+ @Column(nullable = false, length = 250)
+ private String name;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof User user)) return false;
+ return Objects.equals(email, user.email);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(email);
+ }
+}
diff --git a/main/src/main/java/ru/practicum/user/repository/UserRepository.java b/main/src/main/java/ru/practicum/user/repository/UserRepository.java
new file mode 100644
index 0000000..1d4c386
--- /dev/null
+++ b/main/src/main/java/ru/practicum/user/repository/UserRepository.java
@@ -0,0 +1,13 @@
+package ru.practicum.user.repository;
+
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.practicum.user.model.User;
+
+import java.util.List;
+
+public interface UserRepository extends JpaRepository {
+ boolean existsByEmail(String email);
+
+ List findByIdIn(List ids, Pageable pageable);
+}
diff --git a/main/src/main/java/ru/practicum/user/service/UserService.java b/main/src/main/java/ru/practicum/user/service/UserService.java
new file mode 100644
index 0000000..93ef241
--- /dev/null
+++ b/main/src/main/java/ru/practicum/user/service/UserService.java
@@ -0,0 +1,14 @@
+package ru.practicum.user.service;
+
+import ru.practicum.user.dto.NewUserRequest;
+import ru.practicum.user.dto.UserDto;
+
+import java.util.List;
+
+public interface UserService {
+ List getUsers(List ids, Integer from, Integer size);
+
+ UserDto createUser(NewUserRequest newUserRequest);
+
+ void deleteUser(Long id);
+}
diff --git a/main/src/main/java/ru/practicum/user/service/UserServiceImpl.java b/main/src/main/java/ru/practicum/user/service/UserServiceImpl.java
new file mode 100644
index 0000000..dad03eb
--- /dev/null
+++ b/main/src/main/java/ru/practicum/user/service/UserServiceImpl.java
@@ -0,0 +1,61 @@
+package ru.practicum.user.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+import ru.practicum.user.dto.NewUserRequest;
+import ru.practicum.user.dto.UserDto;
+import ru.practicum.user.dto.mapper.UserMapper;
+import ru.practicum.user.repository.UserRepository;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Transactional(readOnly = true)
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class UserServiceImpl implements UserService {
+ private final UserRepository userRepository;
+
+ @Override
+ public List getUsers(List ids, Integer from, Integer size) {
+ PageRequest page = PageRequest.of(from / size, size);
+
+ if (ids != null && !ids.isEmpty()) {
+ return userRepository.findByIdIn(ids, page)
+ .stream()
+ .map(UserMapper::toUserDto)
+ .collect(Collectors.toList());
+ } else {
+ return userRepository.findAll(page)
+ .stream()
+ .map(UserMapper::toUserDto)
+ .collect(Collectors.toList());
+ }
+ }
+
+ @Transactional
+ @Override
+ public UserDto createUser(NewUserRequest newUserRequest) {
+ if (userRepository.existsByEmail(newUserRequest.getEmail())) {
+ log.warn("Пользователь с email - {} уже существует", newUserRequest.getEmail());
+ throw new ConflictException("Пользователь с email - " + newUserRequest.getEmail() + " уже существует");
+ }
+ return UserMapper.toUserDto(userRepository.save(UserMapper.toUser(newUserRequest)));
+ }
+
+ @Transactional
+ @Override
+ public void deleteUser(Long id) {
+ if (!userRepository.existsById(id)) {
+ log.warn("Пользователь с id - {} не найден", id);
+ throw new NotFoundException("Пользователь с id - " + id + " не найден");
+ }
+ userRepository.deleteById(id);
+ }
+}
diff --git a/main/src/main/resources/application.yml b/main/src/main/resources/application.yml
index 443332b..d39638a 100644
--- a/main/src/main/resources/application.yml
+++ b/main/src/main/resources/application.yml
@@ -1,7 +1,17 @@
spring:
+ profiles:
+ active: dev
+
+---
+
+spring:
+ config:
+ activate:
+ on-profile: dev
datasource:
driver-class-name: org.postgresql.Driver
- url: jdbc:postgresql://localhost:6542/maindb
+# url: jdbc:postgresql://localhost:6542/maindb
+ url: jdbc:postgresql://main-db:6542/maindb
username: dbuser2
password: 1234567
jpa:
@@ -15,5 +25,31 @@ spring:
server:
port: 8080
+#client:
+# url: http://localhost:9090
+client:
+ url: http://stats-server:9090
+
+---
+
+spring:
+ config:
+ activate:
+ on-profile: test
+ datasource:
+ driver-class-name: org.h2.Driver
+ url: jdbc:h2:mem:maindb
+ username: dbuser2
+ password: 1234567
+ jpa:
+ hibernate:
+ ddl-auto: create-drop
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.H2Dialect
+
+server:
+ port: 8080
+
client:
url: http://localhost:9090
\ No newline at end of file
diff --git a/main/src/main/resources/schema.sql b/main/src/main/resources/schema.sql
new file mode 100644
index 0000000..d34c148
--- /dev/null
+++ b/main/src/main/resources/schema.sql
@@ -0,0 +1,70 @@
+create table IF NOT EXISTS categories (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ name VARCHAR(50) NOT NULL,
+ CONSTRAINT uq_category_name UNIQUE (name),
+ CONSTRAINT pk_category_id PRIMARY KEY (id)
+);
+
+create table IF NOT EXISTS users (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ email VARCHAR(254) NOT NULL,
+ name VARCHAR(250) NOT NULL,
+ CONSTRAINT uq_user_email UNIQUE (email),
+ CONSTRAINT pk_user_id PRIMARY KEY (id)
+);
+
+create table IF NOT EXISTS locations (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ lat REAL,
+ lon REAL,
+ CONSTRAINT pk_location_id PRIMARY KEY (id)
+);
+
+create table IF NOT EXISTS events (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ annotation VARCHAR(2000) NOT NULL,
+ category_id BIGINT NOT NULL,
+ confirmed_requests BIGINT,
+ created_on TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ description VARCHAR(7000) NOT NULL,
+ event_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ initiator_id BIGINT NOT NULL,
+ location_id BIGINT NOT NULL,
+ paid Boolean NOT NULL,
+ participant_limit INTEGER DEFAULT 0,
+ published_on TIMESTAMP WITHOUT TIME ZONE,
+ request_moderation BOOLEAN DEFAULT true,
+ state VARCHAR(50) NOT NULL,
+ title VARCHAR(120) NOT NULL,
+ CONSTRAINT fk_event_category FOREIGN KEY (category_id) REFERENCES categories (id),
+ CONSTRAINT fk_event_user FOREIGN KEY (initiator_id) REFERENCES users (id),
+ CONSTRAINT fk_event_location FOREIGN KEY (location_id) REFERENCES locations (id),
+ CONSTRAINT pk_event_id PRIMARY KEY (id)
+);
+
+create table IF NOT EXISTS requests (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ create_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ event_id BIGINT NOT NULL,
+ requester_id BIGINT NOT NULL,
+ status VARCHAR(20) NOT NULL,
+ CONSTRAINT fk_request_event FOREIGN KEY (event_id) REFERENCES events (id),
+ CONSTRAINT fk_request_user FOREIGN KEY (requester_id) REFERENCES users (id),
+ CONSTRAINT uq_request UNIQUE (event_id, requester_id),
+ CONSTRAINT pk_request_id PRIMARY KEY (id)
+);
+
+create table IF NOT EXISTS compilations (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ title VARCHAR(50) NOT NULL,
+ pinned BOOLEAN NOT NULL,
+ CONSTRAINT pk_compilation_id PRIMARY KEY (id)
+);
+
+create table IF NOT EXISTS compilations_events(
+ compilation_id BIGINT NOT NULL,
+ event_id BIGINT NOT NULL,
+ CONSTRAINT fk_compilations_events_compilation FOREIGN KEY (compilation_id) REFERENCES compilations (id) ON update CASCADE on delete cascade,
+ CONSTRAINT fk_compilations_events_event FOREIGN KEY (event_id) REFERENCES events (id) ON update CASCADE on delete cascade,
+ CONSTRAINT pk_compilation_event PRIMARY KEY (compilation_id, event_id)
+);
\ No newline at end of file
diff --git a/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java b/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java
new file mode 100644
index 0000000..bc8f9e2
--- /dev/null
+++ b/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java
@@ -0,0 +1,92 @@
+package ru.practicum.category;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import ru.practicum.category.controller.CategoryAdminController;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+import ru.practicum.category.service.CategoryService;
+import ru.practicum.error.ErrorHandler;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@ExtendWith(MockitoExtension.class)
+class CategoryAdminControllerTest {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+ @Mock
+ private CategoryService categoryService;
+ @InjectMocks
+ private CategoryAdminController categoryAdminController;
+ private MockMvc mvc;
+ private CategoryDto categoryDto;
+ private NewCategoryDto newCategoryDto;
+
+ @BeforeEach
+ void setUp() {
+ mapper.findAndRegisterModules();
+ mvc = MockMvcBuilders
+ .standaloneSetup(categoryAdminController)
+ .setControllerAdvice(new ErrorHandler())
+ .build();
+
+ categoryDto = new CategoryDto(1L, "Концерты");
+ newCategoryDto = new NewCategoryDto("Концерты");
+ }
+
+ @Test
+ void mustCreateCategory() throws Exception {
+ when(categoryService.createCategory(any()))
+ .thenReturn(categoryDto);
+
+ mvc.perform(post("/admin/categories")
+ .content(mapper.writeValueAsString(newCategoryDto))
+ .characterEncoding(StandardCharsets.UTF_8)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.name", is("Концерты")));
+ }
+
+ @Test
+ void mustDeleteCategory() throws Exception {
+ mvc.perform(delete("/admin/categories/1"))
+ .andExpect(status().isNoContent());
+
+ verify(categoryService).deleteCategory(1L);
+ }
+
+ @Test
+ void mustUpdateCategory() throws Exception {
+ CategoryDto updated = new CategoryDto(1L, "Фестивали");
+ when(categoryService.updateCategory(eq(1L), any(NewCategoryDto.class)))
+ .thenReturn(updated);
+
+ mvc.perform(patch("/admin/categories/1")
+ .content(mapper.writeValueAsString(newCategoryDto))
+ .characterEncoding(StandardCharsets.UTF_8)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.name", is("Фестивали")));
+ }
+}
diff --git a/main/src/test/java/ru/practicum/category/CategoryDtoTest.java b/main/src/test/java/ru/practicum/category/CategoryDtoTest.java
new file mode 100644
index 0000000..8b9cda9
--- /dev/null
+++ b/main/src/test/java/ru/practicum/category/CategoryDtoTest.java
@@ -0,0 +1,41 @@
+package ru.practicum.category;
+
+import lombok.RequiredArgsConstructor;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.json.JsonTest;
+import org.springframework.boot.test.json.JacksonTester;
+import org.springframework.boot.test.json.JsonContent;
+import org.springframework.test.context.ContextConfiguration;
+import ru.practicum.MainServiceApp;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+@ContextConfiguration(classes = MainServiceApp.class)
+@JsonTest
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class CategoryDtoTest {
+ private final JacksonTester jsonCategoryDto;
+ private final JacksonTester jsonNewCategoryDto;
+
+ @Test
+ void shouldReturnCategoryDto() throws Exception {
+ CategoryDto dto = new CategoryDto(1L, "Выставки");
+
+ JsonContent result = jsonCategoryDto.write(dto);
+
+ assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1);
+ assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Выставки");
+ }
+
+ @Test
+ void shouldReturnNewCategoryDto() throws Exception {
+ NewCategoryDto dto = new NewCategoryDto("Выставки");
+
+ JsonContent result = jsonNewCategoryDto.write(dto);
+
+ assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Выставки");
+ }
+}
diff --git a/main/src/test/java/ru/practicum/category/CategoryMapperTest.java b/main/src/test/java/ru/practicum/category/CategoryMapperTest.java
new file mode 100644
index 0000000..7c3deae
--- /dev/null
+++ b/main/src/test/java/ru/practicum/category/CategoryMapperTest.java
@@ -0,0 +1,31 @@
+package ru.practicum.category;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.Test;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+import ru.practicum.category.dto.mapper.CategoryMapper;
+import ru.practicum.category.model.Category;
+
+
+public class CategoryMapperTest {
+ @Test
+ void shouldMapCategoryToCategoryDto() {
+ Category category = new Category(1L, "Кино");
+
+ CategoryDto result = CategoryMapper.toCategoryDto(category);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L);
+ AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Кино");
+ }
+
+ @Test
+ void shouldMapNewCategoryDtoToCategory() {
+ NewCategoryDto newCategoryDto = new NewCategoryDto("Кино");
+
+ Category result = CategoryMapper.toCategory(newCategoryDto);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isNull();
+ AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Кино");
+ }
+}
diff --git a/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java b/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java
new file mode 100644
index 0000000..3b0ecbf
--- /dev/null
+++ b/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java
@@ -0,0 +1,83 @@
+package ru.practicum.category;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import ru.practicum.category.controller.CategoryPublicController;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.service.CategoryService;
+import ru.practicum.error.ErrorHandler;
+
+import java.util.List;
+
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@ExtendWith(MockitoExtension.class)
+public class CategoryPublicControllerTest {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+ @Mock
+ private CategoryService categoryService;
+ @InjectMocks
+ private CategoryPublicController categoryController;
+ private MockMvc mvc;
+ private CategoryDto categoryDto;
+
+ @BeforeEach
+ void setUp() {
+ mapper.findAndRegisterModules();
+
+ mvc = MockMvcBuilders
+ .standaloneSetup(categoryController)
+ .setControllerAdvice(new ErrorHandler())
+ .build();
+
+ categoryDto = new CategoryDto(1L, "Кино");
+ }
+
+ @Test
+ void mustReturnAllCategories() throws Exception {
+ List categories = List.of(
+ new CategoryDto(1L, "Кино"),
+ new CategoryDto(2L, "Фестиваль")
+ );
+
+ when(categoryService.getCategories(anyInt(), anyInt()))
+ .thenReturn(categories);
+
+ mvc.perform(get("/categories")
+ .param("from", "0")
+ .param("size", "10")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.size()").value(2))
+ .andExpect(jsonPath("$[0].id", is(1)))
+ .andExpect(jsonPath("$[0].name", is("Кино")))
+ .andExpect(jsonPath("$[1].id", is(2)))
+ .andExpect(jsonPath("$[1].name", is("Фестиваль")));
+ }
+
+ @Test
+ void mustReturnCategoryById() throws Exception {
+ when(categoryService.getCategory(1L))
+ .thenReturn(categoryDto);
+
+ mvc.perform(get("/categories/1")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.name", is("Кино")));
+ }
+}
diff --git a/main/src/test/java/ru/practicum/category/CategoryServiceImplTest.java b/main/src/test/java/ru/practicum/category/CategoryServiceImplTest.java
new file mode 100644
index 0000000..8aa6a88
--- /dev/null
+++ b/main/src/test/java/ru/practicum/category/CategoryServiceImplTest.java
@@ -0,0 +1,117 @@
+package ru.practicum.category;
+
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.MainServiceApp;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+import ru.practicum.category.model.Category;
+import ru.practicum.category.service.CategoryService;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+
+import java.util.List;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+
+@Transactional
+@ActiveProfiles("test")
+@SpringBootTest(
+ classes = MainServiceApp.class,
+ webEnvironment = SpringBootTest.WebEnvironment.NONE)
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class CategoryServiceImplTest {
+ private final EntityManager em;
+ private final CategoryService categoryService;
+
+ private Long categoryId;
+
+ @BeforeEach
+ void setUp() {
+ Category category = new Category();
+ category.setName("Кино");
+
+ em.persist(category);
+ em.flush();
+
+ categoryId = category.getId();
+ }
+
+ @Test
+ void mustReturnCategoryById() {
+ CategoryDto categoryDto = categoryService.getCategory(categoryId);
+
+ assertThat(categoryDto.getId()).isEqualTo(categoryId);
+ assertThat(categoryDto.getName()).isEqualTo("Кино");
+ }
+
+ @Test
+ void shouldThrowNotFoundExceptionForInvalidId() {
+ Assertions.assertThrows(NotFoundException.class, () -> categoryService.getCategory(999999L));
+ }
+
+ @Test
+ void mustCreateCategory() {
+ NewCategoryDto newCategoryDto = new NewCategoryDto();
+ newCategoryDto.setName("Фестиваль");
+
+ CategoryDto created = categoryService.createCategory(newCategoryDto);
+
+ assertThat(created.getId()).isNotNull();
+ assertThat(created.getName()).isEqualTo("Фестиваль");
+ }
+
+ @Test
+ void mustThrowConflictExceptionWhenCreatingDuplicateCategory() {
+ NewCategoryDto newCategoryDto = new NewCategoryDto();
+ newCategoryDto.setName("Кино");
+
+ Assertions.assertThrows(ConflictException.class, () -> categoryService.createCategory(newCategoryDto));
+ }
+
+ @Test
+ void mustUpdateCategoryName() {
+ NewCategoryDto updateDto = new NewCategoryDto();
+ updateDto.setName("Спектакль");
+
+ CategoryDto updated = categoryService.updateCategory(categoryId, updateDto);
+
+ assertThat(updated.getId()).isEqualTo(categoryId);
+ assertThat(updated.getName()).isEqualTo("Спектакль");
+ }
+
+ @Test
+ void shouldThrowConflictExceptionWhenUpdatingToExistingName() {
+ Category another = new Category();
+ another.setName("Спектакль");
+ em.persist(another);
+ em.flush();
+
+ NewCategoryDto updateDto = new NewCategoryDto();
+ updateDto.setName("Спектакль");
+
+ Assertions.assertThrows(ConflictException.class, () -> categoryService.updateCategory(categoryId, updateDto));
+ }
+
+ @Test
+ void mustDeleteCategory() {
+ categoryService.deleteCategory(categoryId);
+
+ Assertions.assertThrows(NotFoundException.class, () -> categoryService.getCategory(categoryId));
+ }
+
+ @Test
+ void mustReturnPaginatedCategories() {
+ List categories = categoryService.getCategories(0, 10);
+
+ assertThat(categories.get(0).getName()).isEqualTo("Кино");
+ }
+}
diff --git a/main/src/test/java/ru/practicum/compilation/CompilationAdminControllerTest.java b/main/src/test/java/ru/practicum/compilation/CompilationAdminControllerTest.java
new file mode 100644
index 0000000..c7b9741
--- /dev/null
+++ b/main/src/test/java/ru/practicum/compilation/CompilationAdminControllerTest.java
@@ -0,0 +1,103 @@
+package ru.practicum.compilation;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import ru.practicum.compilation.controller.CompilationAdminController;
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.dto.NewCompilationDto;
+import ru.practicum.compilation.dto.UpdateCompilationRequest;
+import ru.practicum.compilation.service.CompilationService;
+import ru.practicum.error.ErrorHandler;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@ExtendWith(MockitoExtension.class)
+public class CompilationAdminControllerTest {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Mock
+ private CompilationService compilationService;
+
+ @InjectMocks
+ private CompilationAdminController compilationAdminController;
+
+ private MockMvc mvc;
+
+ private CompilationDto compilationDto;
+ private NewCompilationDto newCompilationDto;
+ private UpdateCompilationRequest updateCompilationRequest;
+
+ @BeforeEach
+ void setUp() {
+ mapper.findAndRegisterModules();
+ mvc = MockMvcBuilders
+ .standaloneSetup(compilationAdminController)
+ .setControllerAdvice(new ErrorHandler())
+ .build();
+
+ compilationDto = new CompilationDto(1L, "Подборка сериалов", true, Set.of());
+ newCompilationDto = new NewCompilationDto(Set.of(), true, "Подборка сериалов");
+ updateCompilationRequest = new UpdateCompilationRequest(Set.of(), false, "Следующая подборка");
+ }
+
+ @Test
+ void mustCreateCompilation() throws Exception {
+ when(compilationService.createCompilation(any()))
+ .thenReturn(compilationDto);
+
+ mvc.perform(post("/admin/compilations")
+ .content(mapper.writeValueAsString(newCompilationDto))
+ .characterEncoding(StandardCharsets.UTF_8)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.title", is("Подборка сериалов")))
+ .andExpect(jsonPath("$.pinned", is(true)));
+ }
+
+ @Test
+ void mustDeleteCompilation() throws Exception {
+ mvc.perform(delete("/admin/compilations/1"))
+ .andExpect(status().isNoContent());
+
+ verify(compilationService).deleteCompilation(1L);
+ }
+
+ @Test
+ void mustUpdateCompilation() throws Exception {
+ CompilationDto updated = new CompilationDto(1L, "Следующая подборка", false, Set.of());
+ when(compilationService.updateCompilation(eq(1L), any(UpdateCompilationRequest.class)))
+ .thenReturn(updated);
+
+ mvc.perform(patch("/admin/compilations/1")
+ .content(mapper.writeValueAsString(updateCompilationRequest))
+ .characterEncoding(StandardCharsets.UTF_8)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.title", is("Следующая подборка")))
+ .andExpect(jsonPath("$.pinned", is(false)));
+ }
+}
+
diff --git a/main/src/test/java/ru/practicum/compilation/CompilationDtoTest.java b/main/src/test/java/ru/practicum/compilation/CompilationDtoTest.java
new file mode 100644
index 0000000..636d5b1
--- /dev/null
+++ b/main/src/test/java/ru/practicum/compilation/CompilationDtoTest.java
@@ -0,0 +1,94 @@
+package ru.practicum.compilation;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.json.JsonTest;
+import org.springframework.boot.test.json.JacksonTester;
+import org.springframework.boot.test.json.JsonContent;
+import org.springframework.test.context.ContextConfiguration;
+import ru.practicum.MainServiceApp;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.dto.NewCompilationDto;
+import ru.practicum.compilation.dto.UpdateCompilationRequest;
+import ru.practicum.event.dto.EventShortDto;
+import ru.practicum.user.dto.UserShortDto;
+
+import java.time.LocalDateTime;
+import java.util.Set;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+@JsonTest
+@ContextConfiguration(classes = MainServiceApp.class)
+class CompilationDtoTest {
+
+ @Autowired
+ private JacksonTester jsonCompilationDto;
+
+ @Autowired
+ private JacksonTester jsonNewCompilationDto;
+
+ @Autowired
+ private JacksonTester jsonUpdateCompilationRequest;
+
+ @Test
+ void shouldReturnCompilationDto() throws Exception {
+ EventShortDto eventShortDto = new EventShortDto(
+ 10L,
+ "Интересный концерт",
+ new CategoryDto(1L, "Музыка"),
+ 50L,
+ LocalDateTime.of(2025, 1, 1, 19, 0),
+ new UserShortDto(5L, "Макс"),
+ true,
+ "Концерт в парке",
+ 1000L
+ );
+
+ CompilationDto compilationDto = new CompilationDto(
+ 1L,
+ "Необычные фотозоны",
+ true,
+ Set.of(eventShortDto)
+ );
+
+ JsonContent result = jsonCompilationDto.write(compilationDto);
+
+ assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1);
+ assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Необычные фотозоны");
+ assertThat(result).extractingJsonPathBooleanValue("$.pinned").isEqualTo(true);
+ assertThat(result).extractingJsonPathArrayValue("$.events").hasSize(1);
+ assertThat(result).extractingJsonPathNumberValue("$.events[0].id").isEqualTo(10);
+ }
+
+ @Test
+ void shouldReturnNewCompilationDto() throws Exception {
+ NewCompilationDto dto = new NewCompilationDto(
+ Set.of(100L, 200L),
+ false,
+ "Подборка сериалов"
+ );
+
+ JsonContent result = jsonNewCompilationDto.write(dto);
+
+ assertThat(result).extractingJsonPathArrayValue("$.events").containsExactlyInAnyOrder(100, 200);
+ assertThat(result).extractingJsonPathBooleanValue("$.pinned").isEqualTo(false);
+ assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Подборка сериалов");
+ }
+
+ @Test
+ void shouldReturnUpdateCompilationRequest() throws Exception {
+ UpdateCompilationRequest dto = new UpdateCompilationRequest(
+ Set.of(300L),
+ true,
+ "Обновленная подборка"
+ );
+
+ JsonContent result = jsonUpdateCompilationRequest.write(dto);
+
+ assertThat(result).extractingJsonPathArrayValue("$.events").containsExactly(300);
+ assertThat(result).extractingJsonPathBooleanValue("$.pinned").isEqualTo(true);
+ assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Обновленная подборка");
+ }
+}
\ No newline at end of file
diff --git a/main/src/test/java/ru/practicum/compilation/CompilationMapperTest.java b/main/src/test/java/ru/practicum/compilation/CompilationMapperTest.java
new file mode 100644
index 0000000..4faea0c
--- /dev/null
+++ b/main/src/test/java/ru/practicum/compilation/CompilationMapperTest.java
@@ -0,0 +1,77 @@
+package ru.practicum.compilation;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.Test;
+import ru.practicum.category.model.Category;
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.dto.NewCompilationDto;
+import ru.practicum.compilation.dto.mapper.CompilationMapper;
+import ru.practicum.compilation.model.Compilation;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.state.EventState;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Set;
+
+public class CompilationMapperTest {
+ @Test
+ void shouldMapCompilationToCompilationDto() {
+ Category category = new Category(1L, "Сериал");
+
+ User user = new User();
+ user.setId(10L);
+ user.setName("Макс Иванов");
+ user.setEmail("Max@example.com");
+
+ Event event = new Event();
+ event.setId(100L);
+ event.setCategory(category);
+ event.setInitiator(user);
+ event.setTitle("Друзья");
+ event.setAnnotation("Аннотация");
+ event.setDescription("Описание");
+ event.setCreatedOn(LocalDateTime.now());
+ event.setEventDate(LocalDateTime.now().plusDays(1));
+ event.setState(EventState.PUBLISHED);
+
+ Set events = new HashSet<>();
+ events.add(event);
+
+ Compilation compilation = new Compilation(
+ 1L,
+ "Подборка сериалов",
+ true,
+ events
+ );
+
+ CompilationDto result = CompilationMapper.toCompilationDto(compilation);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L);
+ AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Подборка сериалов");
+ AssertionsForClassTypes.assertThat(result.getPinned()).isTrue();
+ }
+
+ @Test
+ void shouldMapNewCompilationDtoToCompilationPinnedTrue() {
+ NewCompilationDto dto = new NewCompilationDto(Set.of(), true, "Подборка сериалов");
+
+ Compilation result = CompilationMapper.toCompilation(dto);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isNull();
+ AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Подборка сериалов");
+ AssertionsForClassTypes.assertThat(result.getPinned()).isTrue();
+ }
+
+ @Test
+ void shouldMapNewCompilationDtoToCompilationPinnedNull() {
+ NewCompilationDto dto = new NewCompilationDto(Set.of(), null, "Подборка сериалов");
+
+ Compilation result = CompilationMapper.toCompilation(dto);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isNull();
+ AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Подборка сериалов");
+ AssertionsForClassTypes.assertThat(result.getPinned()).isFalse();
+ }
+}
diff --git a/main/src/test/java/ru/practicum/compilation/CompilationPublicControllerTest.java b/main/src/test/java/ru/practicum/compilation/CompilationPublicControllerTest.java
new file mode 100644
index 0000000..2783235
--- /dev/null
+++ b/main/src/test/java/ru/practicum/compilation/CompilationPublicControllerTest.java
@@ -0,0 +1,90 @@
+package ru.practicum.compilation;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import ru.practicum.compilation.controller.CompilationPublicController;
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.service.CompilationService;
+import ru.practicum.error.ErrorHandler;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@ExtendWith(MockitoExtension.class)
+public class CompilationPublicControllerTest {
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Mock
+ private CompilationService compilationService;
+
+ @InjectMocks
+ private CompilationPublicController compilationController;
+
+ private MockMvc mvc;
+
+ private CompilationDto compilationDto1;
+ private CompilationDto compilationDto2;
+
+ @BeforeEach
+ void setUp() {
+ mapper.findAndRegisterModules();
+
+ mvc = MockMvcBuilders
+ .standaloneSetup(compilationController)
+ .setControllerAdvice(new ErrorHandler())
+ .build();
+
+ compilationDto1 = new CompilationDto(1L, "Подборка сериалов", true, Set.of());
+ compilationDto2 = new CompilationDto(2L, "Подборка фильмов", false, Set.of());
+ }
+
+ @Test
+ void mustReturnAllCompilations() throws Exception {
+ List compilations = List.of(compilationDto1, compilationDto2);
+
+ when(compilationService.getCompilations(any(), anyInt(), anyInt()))
+ .thenReturn(compilations);
+
+ mvc.perform(get("/compilations")
+ .param("from", "0")
+ .param("size", "10")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.size()").value(2))
+ .andExpect(jsonPath("$[0].id", is(1)))
+ .andExpect(jsonPath("$[0].title", is("Подборка сериалов")))
+ .andExpect(jsonPath("$[0].pinned", is(true)))
+ .andExpect(jsonPath("$[1].id", is(2)))
+ .andExpect(jsonPath("$[1].title", is("Подборка фильмов")))
+ .andExpect(jsonPath("$[1].pinned", is(false)));
+ }
+
+ @Test
+ void mustReturnCompilationById() throws Exception {
+ when(compilationService.getCompilationById(1L))
+ .thenReturn(compilationDto1);
+
+ mvc.perform(get("/compilations/1")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.title", is("Подборка сериалов")))
+ .andExpect(jsonPath("$.pinned", is(true)));
+ }
+}
diff --git a/main/src/test/java/ru/practicum/compilation/CompilationServiceImplTest.java b/main/src/test/java/ru/practicum/compilation/CompilationServiceImplTest.java
new file mode 100644
index 0000000..f696f64
--- /dev/null
+++ b/main/src/test/java/ru/practicum/compilation/CompilationServiceImplTest.java
@@ -0,0 +1,152 @@
+package ru.practicum.compilation;
+
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.MainServiceApp;
+import ru.practicum.category.model.Category;
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.dto.NewCompilationDto;
+import ru.practicum.compilation.dto.UpdateCompilationRequest;
+import ru.practicum.compilation.model.Compilation;
+import ru.practicum.compilation.service.CompilationService;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.state.EventState;
+import ru.practicum.exception.NotFoundException;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@Transactional
+@ActiveProfiles("test")
+@SpringBootTest(classes = MainServiceApp.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class CompilationServiceImplTest {
+ private final EntityManager em;
+ private final CompilationService compilationService;
+
+ private Long compilationId;
+ private Long eventId;
+
+ @BeforeEach
+ void setUp() {
+ Category category = new Category();
+ category.setName("Сериалы");
+ em.persist(category);
+
+ User user = new User();
+ user.setName("Макс Иванов");
+ user.setEmail("Max@mail.com");
+ em.persist(user);
+
+ Event event = new Event();
+ event.setTitle("Сериал Друзья");
+ event.setAnnotation("Аннотация");
+ event.setDescription("Описание");
+ event.setCreatedOn(LocalDateTime.now());
+ event.setEventDate(LocalDateTime.now().plusDays(1));
+ event.setState(EventState.PUBLISHED);
+ event.setCategory(category);
+ event.setInitiator(user);
+
+ em.persist(event);
+ em.flush();
+
+ eventId = event.getId();
+
+ Compilation compilation = new Compilation();
+ compilation.setTitle("Подборка сериалов");
+ compilation.setPinned(true);
+ compilation.setEvents(Set.of(event));
+
+ em.persist(compilation);
+ em.flush();
+
+ compilationId = compilation.getId();
+ }
+
+ @Test
+ void mustReturnCompilationById() {
+ CompilationDto dto = compilationService.getCompilationById(compilationId);
+
+ assertThat(dto.getId()).isEqualTo(compilationId);
+ assertThat(dto.getTitle()).isEqualTo("Подборка сериалов");
+ assertThat(dto.getPinned()).isTrue();
+ ;
+ }
+
+ @Test
+ void shouldThrowNotFoundExceptionForInvalidId() {
+ assertThrows(NotFoundException.class, () -> compilationService.getCompilationById(999999L));
+ }
+
+ @Test
+ void mustCreateCompilation() {
+ NewCompilationDto newDto = new NewCompilationDto(Set.of(eventId), true, "Следующая подборка");
+
+ CompilationDto created = compilationService.createCompilation(newDto);
+
+ assertThat(created.getId()).isNotNull();
+ assertThat(created.getTitle()).isEqualTo("Следующая подборка");
+ assertThat(created.getPinned()).isTrue();
+ }
+
+ @Test
+ void mustUpdateCompilation() {
+ UpdateCompilationRequest updateDto = new UpdateCompilationRequest(Set.of(eventId), false, "Обновленная подборка");
+
+ CompilationDto updated = compilationService.updateCompilation(compilationId, updateDto);
+
+ assertThat(updated.getId()).isEqualTo(compilationId);
+ assertThat(updated.getTitle()).isEqualTo("Обновленная подборка");
+ assertThat(updated.getPinned()).isFalse();
+ }
+
+ @Test
+ void mustUpdateCompilationWithNullRequest() {
+ CompilationDto updated = compilationService.updateCompilation(compilationId, null);
+
+ assertThat(updated.getId()).isEqualTo(compilationId);
+ assertThat(updated.getTitle()).isEqualTo("Подборка сериалов");
+ }
+
+ @Test
+ void mustDeleteCompilation() {
+ compilationService.deleteCompilation(compilationId);
+
+ assertThrows(NotFoundException.class, () -> compilationService.getCompilationById(compilationId));
+ }
+
+ @Test
+ void mustReturnPaginatedCompilations() {
+ List compilations = compilationService.getCompilations(null, 0, 10);
+
+ assertThat(compilations).isNotEmpty();
+ assertThat(compilations.get(0).getTitle()).isEqualTo("Подборка сериалов");
+ }
+
+ @Test
+ void mustReturnPinnedCompilationsOnly() {
+ List pinned = compilationService.getCompilations(true, 0, 10);
+
+ assertThat(pinned).hasSize(1);
+ assertThat(pinned.get(0).getPinned()).isTrue();
+ }
+
+ @Test
+ void mustReturnEmptyListForUnpinnedFilter() {
+ List unpinned = compilationService.getCompilations(false, 0, 10);
+
+ assertThat(unpinned).isEmpty();
+ }
+}
diff --git a/main/src/test/java/ru/practicum/event/EventAdminControllerTest.java b/main/src/test/java/ru/practicum/event/EventAdminControllerTest.java
new file mode 100644
index 0000000..3ce5b48
--- /dev/null
+++ b/main/src/test/java/ru/practicum/event/EventAdminControllerTest.java
@@ -0,0 +1,126 @@
+package ru.practicum.event;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.error.ErrorHandler;
+import ru.practicum.event.controller.EventAdminController;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.UpdateEventAdminRequest;
+import ru.practicum.event.service.EventService;
+import ru.practicum.location.dto.LocationDto;
+import ru.practicum.user.dto.UserShortDto;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@ExtendWith(MockitoExtension.class)
+class EventAdminControllerTest {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Mock
+ private EventService eventService;
+
+ @InjectMocks
+ private EventAdminController eventAdminController;
+
+ private MockMvc mvc;
+
+ private EventFullDto eventFullDto;
+
+ @BeforeEach
+ void setUp() {
+ mapper.findAndRegisterModules();
+
+ mvc = MockMvcBuilders
+ .standaloneSetup(eventAdminController)
+ .setControllerAdvice(new ErrorHandler())
+ .build();
+
+ CategoryDto categoryDto = new CategoryDto(1L, "Категория");
+ UserShortDto userShortDto = new UserShortDto(1L, "Макс Иванов");
+ LocationDto locationDto = new LocationDto(50.01f, 31.25f);
+
+ eventFullDto = new EventFullDto(
+ 1L,
+ "Аннотация",
+ categoryDto,
+ 5L,
+ LocalDateTime.now(),
+ "Описание",
+ LocalDateTime.now().plusDays(1),
+ userShortDto,
+ locationDto,
+ true,
+ 10,
+ LocalDateTime.now(),
+ true,
+ null,
+ "Заголовок",
+ 100L
+ );
+ }
+
+ @Test
+ void mustReturnAdminEvents() throws Exception {
+ List states = List.of("PUBLISHED");
+ List users = List.of(1L, 2L);
+ List categories = List.of(1L, 2L);
+
+ when(eventService.getAdminEvents(
+ eq(users), eq(states), eq(categories),
+ any(), any(), eq(0), eq(10)
+ )).thenReturn(List.of(eventFullDto));
+
+ mvc.perform(get("/admin/events")
+ .param("users", "1", "2")
+ .param("states", "PUBLISHED")
+ .param("categories", "1", "2")
+ .param("from", "0")
+ .param("size", "10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id").value(eventFullDto.getId()))
+ .andExpect(jsonPath("$[0].annotation").value(eventFullDto.getAnnotation()))
+ .andExpect(jsonPath("$[0].category.id").value(eventFullDto.getCategory().getId()))
+ .andExpect(jsonPath("$[0].category.name").value(eventFullDto.getCategory().getName()))
+ .andExpect(jsonPath("$[0].description").value(eventFullDto.getDescription()))
+ .andExpect(jsonPath("$[0].title").value(eventFullDto.getTitle()));
+ }
+
+ @Test
+ void mustUpdateEventByAdmin() throws Exception {
+ UpdateEventAdminRequest updateRequest = new UpdateEventAdminRequest();
+ updateRequest.setAnnotation("Обновленная аннотация");
+ updateRequest.setDescription("Обновленное описание");
+ updateRequest.setTitle("Обновленный заголовок");
+
+ when(eventService.updateEventByAdmin(eq(1L), any(UpdateEventAdminRequest.class)))
+ .thenReturn(eventFullDto);
+
+ mvc.perform(patch("/admin/events/1")
+ .content(mapper.writeValueAsString(updateRequest))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(eventFullDto.getId()))
+ .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation()))
+ .andExpect(jsonPath("$.description").value(eventFullDto.getDescription()))
+ .andExpect(jsonPath("$.title").value(eventFullDto.getTitle()));
+ }
+}
diff --git a/main/src/test/java/ru/practicum/event/EventDtoTest.java b/main/src/test/java/ru/practicum/event/EventDtoTest.java
new file mode 100644
index 0000000..2cc2048
--- /dev/null
+++ b/main/src/test/java/ru/practicum/event/EventDtoTest.java
@@ -0,0 +1,151 @@
+package ru.practicum.event;
+
+import lombok.RequiredArgsConstructor;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.json.JsonTest;
+import org.springframework.boot.test.json.JacksonTester;
+import org.springframework.boot.test.json.JsonContent;
+import org.springframework.test.context.ContextConfiguration;
+import ru.practicum.MainServiceApp;
+import ru.practicum.event.dto.*;
+import ru.practicum.request.status.RequestStatus;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@JsonTest
+@ContextConfiguration(classes = MainServiceApp.class)
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+class EventDtoTest {
+
+ private final JacksonTester jsonEventFullDto;
+ private final JacksonTester jsonEventShortDto;
+ private final JacksonTester jsonNewEventDto;
+ private final JacksonTester jsonUpdateEventAdminRequest;
+ private final JacksonTester jsonUpdateEventUserRequest;
+ private final JacksonTester jsonEventRequestStatusUpdateRequest;
+ private final JacksonTester jsonEventRequestStatusUpdateResult;
+
+ @Test
+ void shouldReturnEventFullDto() throws Exception {
+ EventFullDto dto = new EventFullDto(
+ 1L, "Аннотация", null, 5L,
+ LocalDateTime.of(2025, 5, 1, 12, 0, 0), "Описание",
+ LocalDateTime.of(2025, 5, 2, 12, 0, 0), null, null,
+ true, 10, LocalDateTime.of(2025, 5, 3, 12, 0, 0),
+ true, null, "Концерт в парке", 100L
+ );
+
+ JsonContent result = jsonEventFullDto.write(dto);
+
+ assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1);
+ assertThat(result).extractingJsonPathStringValue("$.annotation").isEqualTo("Аннотация");
+ assertThat(result).extractingJsonPathStringValue("$.description").isEqualTo("Описание");
+ assertThat(result).extractingJsonPathStringValue("$.createdOn").isEqualTo("2025-05-01 12:00:00");
+ assertThat(result).extractingJsonPathStringValue("$.eventDate").isEqualTo("2025-05-02 12:00:00");
+ assertThat(result).extractingJsonPathStringValue("$.publishedOn").isEqualTo("2025-05-03 12:00:00");
+ assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке");
+ assertThat(result).extractingJsonPathNumberValue("$.confirmedRequests").isEqualTo(5);
+ assertThat(result).extractingJsonPathNumberValue("$.participantLimit").isEqualTo(10);
+ assertThat(result).extractingJsonPathNumberValue("$.views").isEqualTo(100);
+ assertThat(result).extractingJsonPathBooleanValue("$.paid").isTrue();
+ assertThat(result).extractingJsonPathBooleanValue("$.requestModeration").isTrue();
+ }
+
+ @Test
+ void shouldReturnEventShortDto() throws Exception {
+ EventShortDto dto = new EventShortDto(1L, "Аннотация", null, 5L,
+ LocalDateTime.of(2025, 1, 2, 12, 0, 0), null, true, "Концерт в парке", 100L);
+
+ JsonContent result = jsonEventShortDto.write(dto);
+
+ assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1);
+ assertThat(result).extractingJsonPathStringValue("$.annotation").isEqualTo("Аннотация");
+ assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке");
+ assertThat(result).extractingJsonPathBooleanValue("$.paid").isTrue();
+ assertThat(result).extractingJsonPathNumberValue("$.views").isEqualTo(100);
+ }
+
+ @Test
+ void shouldReturnNewEventDto() throws Exception {
+ NewEventDto dto = new NewEventDto(
+ "Аннотация",
+ 1L,
+ "Описание",
+ LocalDateTime.of(2025, 5, 2, 12, 0, 0),
+ null,
+ true,
+ 10,
+ true,
+ "Концерт в парке"
+ );
+
+ JsonContent result = jsonNewEventDto.write(dto);
+
+ assertThat(result).extractingJsonPathStringValue("$.annotation")
+ .isEqualTo("Аннотация");
+ assertThat(result).extractingJsonPathStringValue("$.description")
+ .isEqualTo("Описание");
+ assertThat(result).extractingJsonPathStringValue("$.title")
+ .isEqualTo("Концерт в парке");
+ }
+
+ @Test
+ void shouldReturnUpdateEventAdminRequest() throws Exception {
+ UpdateEventAdminRequest dto = new UpdateEventAdminRequest();
+ dto.setAnnotation("Аннотация");
+ dto.setDescription("Описание");
+ dto.setTitle("Концерт в парке");
+
+ JsonContent result = jsonUpdateEventAdminRequest.write(dto);
+
+ assertThat(result).extractingJsonPathStringValue("$.annotation")
+ .isEqualTo("Аннотация");
+ assertThat(result).extractingJsonPathStringValue("$.description")
+ .isEqualTo("Описание");
+ assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке");
+ }
+
+ @Test
+ void shouldReturnUpdateEventUserRequest() throws Exception {
+ UpdateEventUserRequest dto = new UpdateEventUserRequest();
+ dto.setAnnotation("Аннотация");
+ dto.setDescription("Описание");
+ dto.setTitle("Концерт в парке");
+
+ JsonContent result = jsonUpdateEventUserRequest.write(dto);
+
+ assertThat(result).extractingJsonPathStringValue("$.annotation")
+ .isEqualTo("Аннотация");
+ assertThat(result).extractingJsonPathStringValue("$.description")
+ .isEqualTo("Описание");
+ assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке");
+ }
+
+ @Test
+ void shouldReturnEventRequestStatusUpdateRequest() throws Exception {
+ EventRequestStatusUpdateRequest dto = new EventRequestStatusUpdateRequest(
+ List.of(1L, 2L), RequestStatus.CONFIRMED
+ );
+
+ JsonContent result = jsonEventRequestStatusUpdateRequest.write(dto);
+
+ assertThat(result).extractingJsonPathArrayValue("$.requestIds").containsExactly(1, 2);
+ assertThat(result).extractingJsonPathStringValue("$.status").isEqualTo("CONFIRMED");
+ }
+
+ @Test
+ void shouldReturnEventRequestStatusUpdateResult() throws Exception {
+ EventRequestStatusUpdateResult dto = new EventRequestStatusUpdateResult(
+ List.of(), List.of()
+ );
+
+ JsonContent result = jsonEventRequestStatusUpdateResult.write(dto);
+
+ assertThat(result).extractingJsonPathArrayValue("$.confirmedRequests").isEmpty();
+ assertThat(result).extractingJsonPathArrayValue("$.rejectedRequests").isEmpty();
+ }
+}
diff --git a/main/src/test/java/ru/practicum/event/EventMapperTest.java b/main/src/test/java/ru/practicum/event/EventMapperTest.java
new file mode 100644
index 0000000..428630d
--- /dev/null
+++ b/main/src/test/java/ru/practicum/event/EventMapperTest.java
@@ -0,0 +1,114 @@
+package ru.practicum.event;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.Test;
+import ru.practicum.category.model.Category;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.EventShortDto;
+import ru.practicum.event.dto.NewEventDto;
+import ru.practicum.event.dto.mapper.EventMapper;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.state.EventState;
+import ru.practicum.location.dto.LocationDto;
+import ru.practicum.location.model.Location;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+
+public class EventMapperTest {
+ @Test
+ void shouldMapEventToEventFullDto() {
+ Category category = new Category(1L, "Концерт");
+ User initiator = new User(1L, "Макс Иванов", "Max@mail.ru");
+ Location location = new Location(1L, 50.01f, 31.25f);
+
+ Event event = new Event(
+ 1L,
+ "Аннотация",
+ category,
+ 5L,
+ LocalDateTime.of(2025, 5, 1, 12, 0),
+ "Описание",
+ LocalDateTime.of(2025, 5, 2, 12, 0),
+ initiator,
+ location,
+ true,
+ 10,
+ LocalDateTime.of(2025, 5, 3, 12, 0),
+ true,
+ EventState.PUBLISHED,
+ "Заголовок"
+ );
+
+ EventFullDto result = EventMapper.toEventFullDto(event);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L);
+ AssertionsForClassTypes.assertThat(result.getAnnotation()).isEqualTo("Аннотация");
+ AssertionsForClassTypes.assertThat(result.getDescription()).isEqualTo("Описание");
+ AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Заголовок");
+ AssertionsForClassTypes.assertThat(result.getPaid()).isTrue();
+ AssertionsForClassTypes.assertThat(result.getParticipantLimit()).isEqualTo(10);
+ AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(5L);
+ }
+
+ @Test
+ void shouldMapEventToEventShortDto() {
+ Category category = new Category(1L, "Концерт");
+ User initiator = new User(1L, "Макс Иванов", "Max@mail.ru");
+ Location location = new Location(1L, 50.01f, 31.25f);
+
+ Event event = new Event(
+ 1L,
+ "Аннотация",
+ category,
+ 5L,
+ LocalDateTime.of(2025, 5, 1, 12, 0),
+ "Описание",
+ LocalDateTime.of(2025, 5, 2, 12, 0),
+ initiator,
+ location,
+ true,
+ 10,
+ LocalDateTime.of(2025, 5, 3, 12, 0),
+ true,
+ EventState.PUBLISHED,
+ "Заголовок"
+ );
+
+ EventShortDto result = EventMapper.toEventShortDto(event);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L);
+ AssertionsForClassTypes.assertThat(result.getAnnotation()).isEqualTo("Аннотация");
+ AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Заголовок");
+ AssertionsForClassTypes.assertThat(result.getPaid()).isTrue();
+ AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(5L);
+ }
+
+ @Test
+ void shouldMapNewEventDtoToEvent() {
+ NewEventDto newEventDto = new NewEventDto(
+ "Аннотация",
+ 1L,
+ "Описание",
+ LocalDateTime.of(2025, 5, 2, 12, 0),
+ new LocationDto(50.01f, 31.25f),
+ true,
+ 10,
+ true,
+ "Заголовок"
+ );
+
+ Category category = new Category(1L, "Концерт");
+ User initiator = new User(1L, "Макс Иванов", "Max@mail.ru");
+
+ Event result = EventMapper.toEvent(newEventDto, category, initiator);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isNull();
+ AssertionsForClassTypes.assertThat(result.getAnnotation()).isEqualTo("Аннотация");
+ AssertionsForClassTypes.assertThat(result.getDescription()).isEqualTo("Описание");
+ AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Заголовок");
+ AssertionsForClassTypes.assertThat(result.getPaid()).isTrue();
+ AssertionsForClassTypes.assertThat(result.getParticipantLimit()).isEqualTo(10);
+ AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(0L);
+ }
+}
diff --git a/main/src/test/java/ru/practicum/event/EventPrivateControllerTest.java b/main/src/test/java/ru/practicum/event/EventPrivateControllerTest.java
new file mode 100644
index 0000000..404d3f0
--- /dev/null
+++ b/main/src/test/java/ru/practicum/event/EventPrivateControllerTest.java
@@ -0,0 +1,248 @@
+package ru.practicum.event;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.error.ErrorHandler;
+import ru.practicum.event.controller.EventPrivateController;
+import ru.practicum.event.dto.*;
+import ru.practicum.event.service.EventService;
+import ru.practicum.location.dto.LocationDto;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.status.RequestStatus;
+import ru.practicum.user.dto.UserShortDto;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@ExtendWith(MockitoExtension.class)
+class EventPrivateControllerTest {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Mock
+ private EventService eventService;
+
+ @InjectMocks
+ private EventPrivateController eventPrivateController;
+
+ private MockMvc mvc;
+
+ private EventFullDto eventFullDto;
+ private EventShortDto eventShortDto;
+ private NewEventDto newEventDto;
+ private UpdateEventUserRequest updateEventUserRequest;
+ private EventRequestStatusUpdateRequest updateRequest;
+ private EventRequestStatusUpdateResult updateResult;
+
+ @BeforeEach
+ void setUp() {
+ mapper.findAndRegisterModules();
+
+ mvc = MockMvcBuilders
+ .standaloneSetup(eventPrivateController)
+ .setControllerAdvice(new ErrorHandler())
+ .build();
+
+ CategoryDto categoryDto = new CategoryDto(1L, "Категория");
+ UserShortDto userShortDto = new UserShortDto(1L, "Макс Иванов");
+ LocationDto locationDto = new LocationDto(50.01f, 31.25f);
+
+ eventFullDto = new EventFullDto(1L,
+ "Аннотация",
+ categoryDto,
+ 5L,
+ LocalDateTime.now(),
+ "Описание",
+ LocalDateTime.now().plusDays(1),
+ userShortDto,
+ locationDto,
+ true,
+ 10,
+ LocalDateTime.now(),
+ true,
+ null,
+ "Заголовок",
+ 100L
+ );
+
+ eventShortDto = new EventShortDto(1L,
+ "Аннотация",
+ null,
+ 5L,
+ LocalDateTime.now().plusDays(1),
+ null,
+ true,
+ "Заголовок",
+ 100L
+ );
+
+ newEventDto = new NewEventDto(
+ "Краткое описание, сжатый пересказ сути",
+ 1L,
+ "Наглядное представление о предмете",
+ LocalDateTime.now().plusDays(1),
+ locationDto,
+ true,
+ 10,
+ true,
+ "Заголовок"
+ );
+
+ updateEventUserRequest = new UpdateEventUserRequest(
+ "Обновленная аннотация",
+ 1L,
+ "Обновленное описание",
+ LocalDateTime.now().plusDays(2),
+ null,
+ true,
+ 15,
+ true,
+ null,
+ "Обновленный заголовок"
+ );
+
+ updateRequest = new EventRequestStatusUpdateRequest();
+ updateRequest.setStatus(RequestStatus.CONFIRMED);
+ updateRequest.setRequestIds(List.of(1L, 2L));
+
+ updateResult = new EventRequestStatusUpdateResult(
+ List.of(new ParticipationRequestDto(1L, LocalDateTime.now(), 1L, 1L, RequestStatus.CONFIRMED)),
+ List.of()
+ );
+ }
+
+ @Test
+ void mustReturnEventsByUserId() throws Exception {
+ when(eventService.getEventsByUserId(eq(1L), anyInt(), anyInt()))
+ .thenReturn(List.of(eventShortDto));
+
+ mvc.perform(get("/users/1/events")
+ .param("from", "0")
+ .param("size", "10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id").value(eventShortDto.getId()))
+ .andExpect(jsonPath("$[0].annotation").value(eventShortDto.getAnnotation()))
+ .andExpect(jsonPath("$[0].category").doesNotExist())
+ .andExpect(jsonPath("$[0].confirmedRequests").value(eventShortDto.getConfirmedRequests()))
+ .andExpect(jsonPath("$[0].eventDate").value(eventShortDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$[0].initiator").doesNotExist())
+ .andExpect(jsonPath("$[0].paid").value(eventShortDto.getPaid()))
+ .andExpect(jsonPath("$[0].title").value(eventShortDto.getTitle()))
+ .andExpect(jsonPath("$[0].views").value(eventShortDto.getViews()));
+ }
+
+ @Test
+ void mustCreateEvent() throws Exception {
+ when(eventService.createEvent(eq(1L), any(NewEventDto.class)))
+ .thenReturn(eventFullDto);
+
+ mvc.perform(post("/users/1/events")
+ .content(mapper.writeValueAsString(newEventDto))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id").value(eventFullDto.getId()))
+ .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation()))
+ .andExpect(jsonPath("$.category.id").value(eventFullDto.getCategory().getId()))
+ .andExpect(jsonPath("$.category.name").value(eventFullDto.getCategory().getName()))
+ .andExpect(jsonPath("$.confirmedRequests").value(eventFullDto.getConfirmedRequests()))
+ .andExpect(jsonPath("$.createdOn").value(eventFullDto.getCreatedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$.description").value(eventFullDto.getDescription()))
+ .andExpect(jsonPath("$.eventDate").value(eventFullDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$.initiator.id").value(eventFullDto.getInitiator().getId()))
+ .andExpect(jsonPath("$.initiator.name").value(eventFullDto.getInitiator().getName()))
+ .andExpect(jsonPath("$.location.lat").value(eventFullDto.getLocation().getLat()))
+ .andExpect(jsonPath("$.location.lon").value(eventFullDto.getLocation().getLon()))
+ .andExpect(jsonPath("$.paid").value(eventFullDto.getPaid()))
+ .andExpect(jsonPath("$.participantLimit").value(eventFullDto.getParticipantLimit()))
+ .andExpect(jsonPath("$.publishedOn").value(eventFullDto.getPublishedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$.requestModeration").value(eventFullDto.getRequestModeration()))
+ .andExpect(jsonPath("$.state").doesNotExist())
+ .andExpect(jsonPath("$.title").value(eventFullDto.getTitle()))
+ .andExpect(jsonPath("$.views").value(eventFullDto.getViews()));
+ }
+
+ @Test
+ void mustReturnEventByUserIdAndEventId() throws Exception {
+ when(eventService.getEventByUserIdAndEventId(1L, 1L))
+ .thenReturn(eventFullDto);
+
+ mvc.perform(get("/users/1/events/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(eventFullDto.getId()))
+ .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation()))
+ .andExpect(jsonPath("$.category.id").value(eventFullDto.getCategory().getId()))
+ .andExpect(jsonPath("$.category.name").value(eventFullDto.getCategory().getName()))
+ .andExpect(jsonPath("$.confirmedRequests").value(eventFullDto.getConfirmedRequests()))
+ .andExpect(jsonPath("$.createdOn").value(eventFullDto.getCreatedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$.description").value(eventFullDto.getDescription()))
+ .andExpect(jsonPath("$.eventDate").value(eventFullDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$.initiator.id").value(eventFullDto.getInitiator().getId()))
+ .andExpect(jsonPath("$.initiator.name").value(eventFullDto.getInitiator().getName()))
+ .andExpect(jsonPath("$.location.lat").value(eventFullDto.getLocation().getLat()))
+ .andExpect(jsonPath("$.location.lon").value(eventFullDto.getLocation().getLon()))
+ .andExpect(jsonPath("$.paid").value(eventFullDto.getPaid()))
+ .andExpect(jsonPath("$.participantLimit").value(eventFullDto.getParticipantLimit()))
+ .andExpect(jsonPath("$.publishedOn").value(eventFullDto.getPublishedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$.requestModeration").value(eventFullDto.getRequestModeration()))
+ .andExpect(jsonPath("$.state").doesNotExist())
+ .andExpect(jsonPath("$.title").value(eventFullDto.getTitle()))
+ .andExpect(jsonPath("$.views").value(eventFullDto.getViews()));
+ }
+
+ @Test
+ void mustUpdateEventByUserIdAndEventId() throws Exception {
+ when(eventService.updateEventByUserIdAndEventId(eq(1L), eq(1L), any(UpdateEventUserRequest.class)))
+ .thenReturn(eventFullDto);
+
+ mvc.perform(patch("/users/1/events/1")
+ .content(mapper.writeValueAsString(updateEventUserRequest))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(eventFullDto.getId()))
+ .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation()))
+ .andExpect(jsonPath("$.description").value(eventFullDto.getDescription()))
+ .andExpect(jsonPath("$.title").value(eventFullDto.getTitle()));
+ }
+
+ @Test
+ void mustReturnEventRequests() throws Exception {
+ when(eventService.getEventRequestsByUserIdAndEventId(1L, 1L))
+ .thenReturn(List.of());
+
+ mvc.perform(get("/users/1/events/1/requests"))
+ .andExpect(status().isOk())
+ .andExpect(content().json("[]"));
+ }
+
+ @Test
+ void mustChangeStatus() throws Exception {
+ when(eventService.changeStatus(eq(1L), eq(1L), any(EventRequestStatusUpdateRequest.class)))
+ .thenReturn(updateResult);
+
+ mvc.perform(patch("/users/1/events/1/requests")
+ .content(mapper.writeValueAsString(updateRequest))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.confirmedRequests[0].id").value(updateResult.getConfirmedRequests().get(0).getId()))
+ .andExpect(jsonPath("$.confirmedRequests[0].event").value(updateResult.getConfirmedRequests().get(0).getEvent()))
+ .andExpect(jsonPath("$.confirmedRequests[0].requester").value(updateResult.getConfirmedRequests().get(0).getRequester()))
+ .andExpect(jsonPath("$.confirmedRequests[0].status").value(updateResult.getConfirmedRequests().get(0).getStatus().toString()))
+ .andExpect(jsonPath("$.rejectedRequests").isEmpty());
+ }
+}
diff --git a/main/src/test/java/ru/practicum/event/EventPublicControllerTest.java b/main/src/test/java/ru/practicum/event/EventPublicControllerTest.java
new file mode 100644
index 0000000..775c92a
--- /dev/null
+++ b/main/src/test/java/ru/practicum/event/EventPublicControllerTest.java
@@ -0,0 +1,154 @@
+package ru.practicum.event;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.error.ErrorHandler;
+import ru.practicum.event.controller.EventPublicController;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.EventShortDto;
+import ru.practicum.event.service.EventService;
+import ru.practicum.location.dto.LocationDto;
+import ru.practicum.user.dto.UserShortDto;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@ExtendWith(MockitoExtension.class)
+class EventPublicControllerTest {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Mock
+ private EventService eventService;
+
+ @InjectMocks
+ private EventPublicController eventPublicController;
+
+ private MockMvc mvc;
+
+ private EventFullDto eventFullDto;
+ private EventShortDto eventShortDto;
+
+ @BeforeEach
+ void setUp() {
+ mapper.findAndRegisterModules();
+
+ mvc = MockMvcBuilders
+ .standaloneSetup(eventPublicController)
+ .setControllerAdvice(new ErrorHandler())
+ .build();
+
+ CategoryDto categoryDto = new CategoryDto(1L, "Категория");
+ UserShortDto userShortDto = new UserShortDto(1L, "Макс Иванов");
+ LocationDto locationDto = new LocationDto(50.01f, 31.25f);
+
+ eventFullDto = new EventFullDto(
+ 1L,
+ "Аннотация",
+ categoryDto,
+ 10L,
+ LocalDateTime.now(),
+ "Описание",
+ LocalDateTime.now().plusDays(1),
+ userShortDto,
+ locationDto,
+ true,
+ 100,
+ LocalDateTime.now(),
+ true,
+ null,
+ "Заголовок",
+ 500L
+ );
+
+ eventShortDto = new EventShortDto(
+ 1L,
+ "Аннотация",
+ categoryDto,
+ 10L,
+ LocalDateTime.now().plusDays(1),
+ userShortDto,
+ true,
+ "Заголовок",
+ 500L
+ );
+ }
+
+ @Test
+ void mustReturnPublicEvents() throws Exception {
+ when(eventService.getEvents(
+ anyString(), anyList(), anyBoolean(),
+ any(), any(),
+ anyBoolean(), anyString(),
+ anyInt(), anyInt(),
+ any(HttpServletRequest.class)
+ )).thenReturn(List.of(eventShortDto));
+
+ mvc.perform(get("/events")
+ .param("text", "текст для поиска")
+ .param("categories", "1")
+ .param("paid", "true")
+ .param("rangeStart", "2025-05-22 12:00:00")
+ .param("rangeEnd", "2025-05-23 12:00:00")
+ .param("onlyAvailable", "true")
+ .param("sort", "EVENT_DATE")
+ .param("from", "0")
+ .param("size", "10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id").value(eventShortDto.getId()))
+ .andExpect(jsonPath("$[0].annotation").value(eventShortDto.getAnnotation()))
+ .andExpect(jsonPath("$[0].category.id").value(eventShortDto.getCategory().getId()))
+ .andExpect(jsonPath("$[0].category.name").value(eventShortDto.getCategory().getName()))
+ .andExpect(jsonPath("$[0].confirmedRequests").value(eventShortDto.getConfirmedRequests()))
+ .andExpect(jsonPath("$[0].eventDate").value(eventShortDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$[0].initiator.id").value(eventShortDto.getInitiator().getId()))
+ .andExpect(jsonPath("$[0].initiator.name").value(eventShortDto.getInitiator().getName()))
+ .andExpect(jsonPath("$[0].paid").value(eventShortDto.getPaid()))
+ .andExpect(jsonPath("$[0].title").value(eventShortDto.getTitle()))
+ .andExpect(jsonPath("$[0].views").value(eventShortDto.getViews()));
+ }
+
+ @Test
+ void mustReturnPublicEventById() throws Exception {
+ when(eventService.getEventById(eq(1L), any(HttpServletRequest.class)))
+ .thenReturn(eventFullDto);
+
+ mvc.perform(get("/events/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(eventFullDto.getId()))
+ .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation()))
+ .andExpect(jsonPath("$.category.id").value(eventFullDto.getCategory().getId()))
+ .andExpect(jsonPath("$.category.name").value(eventFullDto.getCategory().getName()))
+ .andExpect(jsonPath("$.confirmedRequests").value(eventFullDto.getConfirmedRequests()))
+ .andExpect(jsonPath("$.createdOn").value(eventFullDto.getCreatedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$.description").value(eventFullDto.getDescription()))
+ .andExpect(jsonPath("$.eventDate").value(eventFullDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$.initiator.id").value(eventFullDto.getInitiator().getId()))
+ .andExpect(jsonPath("$.initiator.name").value(eventFullDto.getInitiator().getName()))
+ .andExpect(jsonPath("$.location.lat").value(eventFullDto.getLocation().getLat()))
+ .andExpect(jsonPath("$.location.lon").value(eventFullDto.getLocation().getLon()))
+ .andExpect(jsonPath("$.paid").value(eventFullDto.getPaid()))
+ .andExpect(jsonPath("$.participantLimit").value(eventFullDto.getParticipantLimit()))
+ .andExpect(jsonPath("$.publishedOn").value(eventFullDto.getPublishedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))))
+ .andExpect(jsonPath("$.requestModeration").value(eventFullDto.getRequestModeration()))
+ .andExpect(jsonPath("$.state").doesNotExist())
+ .andExpect(jsonPath("$.title").value(eventFullDto.getTitle()))
+ .andExpect(jsonPath("$.views").value(eventFullDto.getViews()));
+ }
+}
diff --git a/main/src/test/java/ru/practicum/event/EventServiceImplTest.java b/main/src/test/java/ru/practicum/event/EventServiceImplTest.java
new file mode 100644
index 0000000..3bcafb3
--- /dev/null
+++ b/main/src/test/java/ru/practicum/event/EventServiceImplTest.java
@@ -0,0 +1,332 @@
+package ru.practicum.event;
+
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.MainServiceApp;
+import ru.practicum.category.model.Category;
+import ru.practicum.event.dto.*;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.service.EventService;
+import ru.practicum.event.state.EventState;
+import ru.practicum.event.state.EventStateActionSolution;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.ValidationException;
+import ru.practicum.location.dto.LocationDto;
+import ru.practicum.location.model.Location;
+import ru.practicum.request.model.Request;
+import ru.practicum.request.status.RequestStatus;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+@Transactional
+@ActiveProfiles("test")
+@SpringBootTest(
+ classes = MainServiceApp.class,
+ webEnvironment = SpringBootTest.WebEnvironment.NONE)
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class EventServiceImplTest {
+
+ private final EntityManager em;
+ private final EventService eventService;
+ private Long userId;
+ private Long categoryId;
+ private Long locationId;
+
+ @BeforeEach
+ void setUp() {
+ User user = new User();
+ user.setName("Макс Иванов");
+ user.setEmail("Max@mail.com");
+ em.persist(user);
+
+ userId = user.getId();
+
+ Category category = new Category();
+ category.setName("Кино");
+ em.persist(category);
+ categoryId = category.getId();
+
+ Location location = new Location();
+ location.setLat(50.01f);
+ location.setLon(31.25f);
+ em.persist(location);
+ locationId = location.getId();
+
+ em.flush();
+ }
+
+ @Test
+ void mustCreateEvent() {
+ NewEventDto dto = new NewEventDto(
+ "Аннотация",
+ categoryId,
+ "Описание",
+ LocalDateTime.now().plusDays(1),
+ new LocationDto(50.01f, 31.25f),
+ true,
+ 10,
+ true,
+ "Заголовок"
+ );
+
+ EventFullDto created = eventService.createEvent(userId, dto);
+
+ assertThat(created.getId()).isNotNull();
+ assertThat(created.getAnnotation()).isEqualTo(dto.getAnnotation());
+ assertThat(created.getTitle()).isEqualTo(dto.getTitle());
+ assertThat(created.getCategory().getId()).isEqualTo(categoryId);
+ }
+
+ @Test
+ void mustGetEventByUserIdAndEventId() {
+ Event event = new Event();
+ event.setAnnotation("Аннотация");
+ event.setCategory(em.find(Category.class, categoryId));
+ event.setCreatedOn(LocalDateTime.now());
+ event.setDescription("Описание");
+ event.setEventDate(LocalDateTime.now().plusDays(1));
+ event.setInitiator(em.find(User.class, userId));
+ event.setLocation(em.find(Location.class, locationId));
+ event.setPaid(true);
+ event.setParticipantLimit(10);
+ event.setRequestModeration(true);
+ event.setState(EventState.PENDING);
+ event.setTitle("Заголовок");
+
+ em.persist(event);
+ em.flush();
+
+ EventFullDto found = eventService.getEventByUserIdAndEventId(userId, event.getId());
+
+ assertThat(found.getId()).isEqualTo(event.getId());
+ assertThat(found.getAnnotation()).isEqualTo(event.getAnnotation());
+ assertThat(found.getTitle()).isEqualTo(event.getTitle());
+ }
+
+ @Test
+ void mustUpdateEventByUser() {
+ Event event = new Event();
+ event.setAnnotation("Аннотация");
+ event.setCategory(em.find(Category.class, categoryId));
+ event.setCreatedOn(LocalDateTime.now());
+ event.setDescription("Описание");
+ event.setEventDate(LocalDateTime.now().plusDays(1));
+ event.setInitiator(em.find(User.class, userId));
+ event.setLocation(em.find(Location.class, locationId));
+ event.setPaid(true);
+ event.setParticipantLimit(10);
+ event.setRequestModeration(true);
+ event.setState(EventState.PENDING);
+ event.setTitle("Заголовок");
+
+ em.persist(event);
+ em.flush();
+
+ UpdateEventUserRequest updateDto = new UpdateEventUserRequest();
+ updateDto.setAnnotation("Обновленная аннотация");
+ updateDto.setTitle("Обновленный заголовок");
+
+ EventFullDto updated = eventService.updateEventByUserIdAndEventId(userId, event.getId(), updateDto);
+
+ assertThat(updated.getAnnotation()).isEqualTo("Обновленная аннотация");
+ assertThat(updated.getTitle()).isEqualTo("Обновленный заголовок");
+ }
+
+ @Test
+ void shouldThrowValidationExceptionForInvalidEventDate() {
+ NewEventDto dto = new NewEventDto(
+ "Аннотация",
+ categoryId,
+ "Описание",
+ LocalDateTime.now().plusMinutes(30),
+ new LocationDto(50.01f, 31.25f),
+ true,
+ 10,
+ true,
+ "Заголовок"
+ );
+
+ Assertions.assertThrows(ValidationException.class, () -> eventService.createEvent(userId, dto));
+ }
+
+ @Test
+ void mustReturnEventsByUserId() {
+ Event event = new Event();
+ event.setAnnotation("Аннотация");
+ event.setCategory(em.find(Category.class, categoryId));
+ event.setCreatedOn(LocalDateTime.now());
+ event.setDescription("Описание");
+ event.setEventDate(LocalDateTime.now().plusDays(1));
+ event.setInitiator(em.find(User.class, userId));
+ event.setLocation(em.find(Location.class, locationId));
+ event.setPaid(true);
+ event.setParticipantLimit(10);
+ event.setRequestModeration(true);
+ event.setState(EventState.PENDING);
+ event.setTitle("Заголовок");
+
+ em.persist(event);
+ em.flush();
+
+ List events = eventService.getEventsByUserId(userId, 0, 10);
+
+ assertThat(events).isNotEmpty();
+ assertThat(events.get(0).getId()).isEqualTo(event.getId());
+ }
+
+ @Test
+ void mustReturnAdminEvents() {
+ Event event = new Event();
+ event.setAnnotation("Аннотация");
+ event.setCategory(em.find(Category.class, categoryId));
+ event.setCreatedOn(LocalDateTime.now());
+ event.setDescription("Описание");
+ event.setEventDate(LocalDateTime.now().plusDays(1));
+ event.setInitiator(em.find(User.class, userId));
+ event.setLocation(em.find(Location.class, locationId));
+ event.setPaid(true);
+ event.setParticipantLimit(10);
+ event.setRequestModeration(true);
+ event.setState(EventState.PENDING);
+ event.setTitle("Заголовок");
+
+ em.persist(event);
+ em.flush();
+
+ List events = eventService.getAdminEvents(
+ List.of(userId),
+ List.of("PENDING"),
+ List.of(categoryId),
+ null,
+ null,
+ 0,
+ 10
+ );
+
+ assertThat(events).hasSize(1);
+ EventFullDto dto = events.get(0);
+ assertThat(dto.getId()).isEqualTo(event.getId());
+ assertThat(dto.getTitle()).isEqualTo("Заголовок");
+ assertThat(dto.getCategory().getId()).isEqualTo(categoryId);
+ }
+
+ @Test
+ void mustUpdateEventByAdmin() {
+ Event event = new Event();
+ event.setAnnotation("Аннотация");
+ event.setCategory(em.find(Category.class, categoryId));
+ event.setCreatedOn(LocalDateTime.now());
+ event.setDescription("Описание");
+ event.setEventDate(LocalDateTime.now().plusDays(1));
+ event.setInitiator(em.find(User.class, userId));
+ event.setLocation(em.find(Location.class, locationId));
+ event.setPaid(true);
+ event.setParticipantLimit(10);
+ event.setRequestModeration(true);
+ event.setState(EventState.PENDING);
+ event.setTitle("Заголовок");
+
+ em.persist(event);
+ em.flush();
+
+ UpdateEventAdminRequest updateRequest = new UpdateEventAdminRequest();
+ updateRequest.setTitle("Обновленный заголовок");
+ updateRequest.setAnnotation("Обновленная аннотация");
+ updateRequest.setStateAction(EventStateActionSolution.PUBLISH_EVENT);
+
+ EventFullDto updated = eventService.updateEventByAdmin(event.getId(), updateRequest);
+
+ assertThat(updated.getTitle()).isEqualTo("Обновленный заголовок");
+ assertThat(updated.getAnnotation()).isEqualTo("Обновленная аннотация");
+ assertThat(updated.getState()).isEqualTo(EventState.PUBLISHED);
+ }
+
+ @Test
+ void shouldThrowConflictExceptionWhenPublishingWrongState() {
+ Event event = new Event();
+ event.setAnnotation("Аннотация");
+ event.setCategory(em.find(Category.class, categoryId));
+ event.setCreatedOn(LocalDateTime.now());
+ event.setDescription("Описание");
+ event.setEventDate(LocalDateTime.now().plusDays(1));
+ event.setInitiator(em.find(User.class, userId));
+ event.setLocation(em.find(Location.class, locationId));
+ event.setPaid(true);
+ event.setParticipantLimit(10);
+ event.setRequestModeration(true);
+ event.setState(EventState.PUBLISHED);
+ event.setTitle("Заголовок");
+
+ em.persist(event);
+ em.flush();
+
+ UpdateEventAdminRequest updateRequest = new UpdateEventAdminRequest();
+ updateRequest.setStateAction(EventStateActionSolution.PUBLISH_EVENT);
+
+ Assertions.assertThrows(ConflictException.class,
+ () -> eventService.updateEventByAdmin(event.getId(), updateRequest));
+ }
+
+ @Test
+ void mustChangeRequestStatus() {
+ Event event = new Event();
+ event.setAnnotation("Аннотация");
+ event.setCategory(em.find(Category.class, categoryId));
+ event.setCreatedOn(LocalDateTime.now());
+ event.setDescription("Описание");
+ event.setEventDate(LocalDateTime.now().plusDays(1));
+ event.setInitiator(em.find(User.class, userId));
+ event.setLocation(em.find(Location.class, locationId));
+ event.setPaid(true);
+ event.setParticipantLimit(1);
+ event.setRequestModeration(true);
+ event.setState(EventState.PENDING);
+ event.setTitle("Заголовок");
+
+ em.persist(event);
+ User requester1 = new User();
+ requester1.setName("Макс Иванов");
+ requester1.setEmail("Max@mail.ru");
+ em.persist(requester1);
+
+ User requester2 = new User();
+ requester2.setName("Ирина Дубинина");
+ requester2.setEmail("Iren@mail.ru");
+ em.persist(requester2);
+
+ Request request1 = new Request();
+ request1.setEvent(event);
+ request1.setRequester(requester1);
+ request1.setStatus(RequestStatus.PENDING);
+
+ Request request2 = new Request();
+ request2.setEvent(event);
+ request2.setRequester(requester2);
+ request2.setStatus(RequestStatus.PENDING);
+
+ em.persist(request1);
+ em.persist(request2);
+ em.flush();
+
+ EventRequestStatusUpdateRequest statusRequest = new EventRequestStatusUpdateRequest();
+ statusRequest.setRequestIds(List.of(request1.getId(), request2.getId()));
+ statusRequest.setStatus(RequestStatus.CONFIRMED);
+
+ EventRequestStatusUpdateResult result = eventService.changeStatus(event.getInitiator().getId(), event.getId(), statusRequest);
+
+ assertThat(result.getConfirmedRequests()).hasSize(1);
+ assertThat(result.getRejectedRequests()).hasSize(1);
+ }
+}
\ No newline at end of file
diff --git a/main/src/test/java/ru/practicum/location/LocationDtoTest.java b/main/src/test/java/ru/practicum/location/LocationDtoTest.java
new file mode 100644
index 0000000..26b411f
--- /dev/null
+++ b/main/src/test/java/ru/practicum/location/LocationDtoTest.java
@@ -0,0 +1,37 @@
+package ru.practicum.location;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.json.JsonTest;
+import org.springframework.boot.test.json.JacksonTester;
+import org.springframework.boot.test.json.JsonContent;
+import ru.practicum.location.dto.LocationDto;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+@JsonTest
+class LocationDtoTest {
+
+ @Autowired
+ private JacksonTester jsonLocationDto;
+
+ @Test
+ void shouldSerializeLocationDto() throws Exception {
+ LocationDto dto = new LocationDto(50.01f, 31.25f);
+
+ JsonContent result = jsonLocationDto.write(dto);
+
+ assertThat(result).extractingJsonPathNumberValue("$.lat").isEqualTo(50.01);
+ assertThat(result).extractingJsonPathNumberValue("$.lon").isEqualTo(31.25);
+ }
+
+ @Test
+ void shouldDeserializeLocationDto() throws Exception {
+ String json = "{\"lat\": 50.01, \"lon\": 31.25}";
+
+ LocationDto dto = jsonLocationDto.parseObject(json);
+
+ assertThat(dto.getLat()).isEqualTo(50.01f);
+ assertThat(dto.getLon()).isEqualTo(31.25f);
+ }
+}
diff --git a/main/src/test/java/ru/practicum/location/LocationMapperTest.java b/main/src/test/java/ru/practicum/location/LocationMapperTest.java
new file mode 100644
index 0000000..230dccd
--- /dev/null
+++ b/main/src/test/java/ru/practicum/location/LocationMapperTest.java
@@ -0,0 +1,31 @@
+package ru.practicum.location;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.Test;
+import ru.practicum.location.dto.LocationDto;
+import ru.practicum.location.dto.mapper.LocationMapper;
+import ru.practicum.location.model.Location;
+
+class LocationMapperTest {
+
+ @Test
+ void shouldLocationToLocationDto() {
+ Location location = new Location(1L, 50.01f, 31.25f);
+
+ LocationDto result = LocationMapper.toLocationDto(location);
+
+ AssertionsForClassTypes.assertThat(result.getLat()).isEqualTo(50.01f);
+ AssertionsForClassTypes.assertThat(result.getLon()).isEqualTo(31.25f);
+ }
+
+ @Test
+ void shouldLocationDtoToLocation() {
+ LocationDto dto = new LocationDto(50.01f, 31.25f);
+
+ Location result = LocationMapper.toLocation(dto);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isNull();
+ AssertionsForClassTypes.assertThat(result.getLat()).isEqualTo(50.01f);
+ AssertionsForClassTypes.assertThat(result.getLon()).isEqualTo(31.25f);
+ }
+}
diff --git a/main/src/test/java/ru/practicum/request/RequestDtoTest.java b/main/src/test/java/ru/practicum/request/RequestDtoTest.java
new file mode 100644
index 0000000..4315154
--- /dev/null
+++ b/main/src/test/java/ru/practicum/request/RequestDtoTest.java
@@ -0,0 +1,41 @@
+package ru.practicum.request;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.json.JsonTest;
+import org.springframework.boot.test.json.JacksonTester;
+import org.springframework.boot.test.json.JsonContent;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.status.RequestStatus;
+
+import java.time.LocalDateTime;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+@JsonTest
+class RequestDtoTest {
+
+ @Autowired
+ private JacksonTester jsonParticipationRequestDto;
+
+ @Test
+ void shouldReturnParticipationRequestDto() throws Exception {
+ LocalDateTime created = LocalDateTime.of(2025, 1, 1, 12, 30, 45);
+ ParticipationRequestDto dto = new ParticipationRequestDto(
+ 10L,
+ created,
+ 1L,
+ 5L,
+ RequestStatus.CONFIRMED
+ );
+
+ JsonContent result = jsonParticipationRequestDto.write(dto);
+
+ assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(10);
+ assertThat(result).extractingJsonPathStringValue("$.created")
+ .isEqualTo("2025-01-01 12:30:45");
+ assertThat(result).extractingJsonPathNumberValue("$.event").isEqualTo(1);
+ assertThat(result).extractingJsonPathNumberValue("$.requester").isEqualTo(5);
+ assertThat(result).extractingJsonPathStringValue("$.status").isEqualTo("CONFIRMED");
+ }
+}
diff --git a/main/src/test/java/ru/practicum/request/RequestMapperTest.java b/main/src/test/java/ru/practicum/request/RequestMapperTest.java
new file mode 100644
index 0000000..610372f
--- /dev/null
+++ b/main/src/test/java/ru/practicum/request/RequestMapperTest.java
@@ -0,0 +1,39 @@
+package ru.practicum.request;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.Test;
+import ru.practicum.event.model.Event;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.dto.mapper.RequestMapper;
+import ru.practicum.request.model.Request;
+import ru.practicum.request.status.RequestStatus;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+
+public class RequestMapperTest {
+ @Test
+ void shouldRequestToParticipationRequestDto() {
+ LocalDateTime created = LocalDateTime.of(2025, 1, 1, 12, 0, 0);
+
+ User requester = new User(2L, "Max@mail.com", "Макс Иванов");
+ Event event = new Event();
+ event.setId(3L);
+
+ Request request = new Request(
+ 1L,
+ created,
+ event,
+ requester,
+ RequestStatus.PENDING
+ );
+
+ ParticipationRequestDto result = RequestMapper.toParticipationRequestDto(request);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L);
+ AssertionsForClassTypes.assertThat(result.getCreated()).isEqualTo(created);
+ AssertionsForClassTypes.assertThat(result.getEvent()).isEqualTo(3L);
+ AssertionsForClassTypes.assertThat(result.getRequester()).isEqualTo(2L);
+ AssertionsForClassTypes.assertThat(result.getStatus()).isEqualTo(RequestStatus.PENDING);
+ }
+}
diff --git a/main/src/test/java/ru/practicum/request/RequestPrivateControllerTest.java b/main/src/test/java/ru/practicum/request/RequestPrivateControllerTest.java
new file mode 100644
index 0000000..1967711
--- /dev/null
+++ b/main/src/test/java/ru/practicum/request/RequestPrivateControllerTest.java
@@ -0,0 +1,104 @@
+package ru.practicum.request;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import ru.practicum.error.ErrorHandler;
+import ru.practicum.request.controller.RequestPrivateController;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.service.RequestService;
+import ru.practicum.request.status.RequestStatus;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@ExtendWith(MockitoExtension.class)
+public class RequestPrivateControllerTest {
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Mock
+ private RequestService requestService;
+
+ @InjectMocks
+ private RequestPrivateController requestPrivateController;
+
+ private MockMvc mvc;
+ private ParticipationRequestDto requestDto;
+
+ @BeforeEach
+ void setUp() {
+ mapper.findAndRegisterModules();
+ mvc = MockMvcBuilders
+ .standaloneSetup(requestPrivateController)
+ .setControllerAdvice(new ErrorHandler())
+ .build();
+
+ requestDto = new ParticipationRequestDto(
+ 1L,
+ LocalDateTime.of(2025, 5, 1, 12, 0, 0),
+ 1L,
+ 5L,
+ RequestStatus.PENDING
+ );
+ }
+
+ @Test
+ void mustGetRequestsByUserId() throws Exception {
+ when(requestService.getRequestsByUserId(anyLong()))
+ .thenReturn(List.of(requestDto));
+
+ mvc.perform(get("/users/5/requests")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id", is(1)))
+ .andExpect(jsonPath("$[0].event", is(1)))
+ .andExpect(jsonPath("$[0].requester", is(5)))
+ .andExpect(jsonPath("$[0].status", is("PENDING")))
+ .andExpect(jsonPath("$[0].created", is("2025-05-01 12:00:00")));
+ }
+
+ @Test
+ void mustCreateRequest() throws Exception {
+ when(requestService.createRequest(anyLong(), anyLong()))
+ .thenReturn(requestDto);
+
+ mvc.perform(post("/users/5/requests")
+ .param("eventId", "100")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.event", is(1)))
+ .andExpect(jsonPath("$.requester", is(5)))
+ .andExpect(jsonPath("$.status", is("PENDING")))
+ .andExpect(jsonPath("$.created", is("2025-05-01 12:00:00")));
+ }
+
+ @Test
+ void mustCancelRequest() throws Exception {
+ when(requestService.cancelRequest(anyLong(), anyLong()))
+ .thenReturn(requestDto);
+
+ mvc.perform(patch("/users/5/requests/1/cancel")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.event", is(1)))
+ .andExpect(jsonPath("$.requester", is(5)))
+ .andExpect(jsonPath("$.status", is("PENDING")))
+ .andExpect(jsonPath("$.created", is("2025-05-01 12:00:00")));
+ }
+}
diff --git a/main/src/test/java/ru/practicum/request/RequestServiceImplTest.java b/main/src/test/java/ru/practicum/request/RequestServiceImplTest.java
new file mode 100644
index 0000000..9b30016
--- /dev/null
+++ b/main/src/test/java/ru/practicum/request/RequestServiceImplTest.java
@@ -0,0 +1,140 @@
+package ru.practicum.request;
+
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.MainServiceApp;
+import ru.practicum.category.model.Category;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.state.EventState;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+import ru.practicum.location.model.Location;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.service.RequestService;
+import ru.practicum.request.status.RequestStatus;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@Transactional
+@ActiveProfiles("test")
+@SpringBootTest(classes = MainServiceApp.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+public class RequestServiceImplTest {
+ private final EntityManager em;
+ private final RequestService requestService;
+
+ private User user;
+ private User initiator;
+ private Category category;
+ private Location location;
+ private Event event;
+
+ @BeforeEach
+ void setUp() {
+ category = new Category();
+ category.setName("Концерт");
+ em.persist(category);
+
+ location = new Location();
+ location.setLat(50.01f);
+ location.setLon(31.25f);
+ em.persist(location);
+
+ user = new User();
+ user.setName("Макс Иванов");
+ user.setEmail("Max@mail.com");
+ em.persist(user);
+
+ initiator = new User();
+ initiator.setName("Ирина Дубова");
+ initiator.setEmail("Iren@mail.com");
+ em.persist(initiator);
+
+ event = new Event();
+ event.setTitle("Концерт в парке");
+ event.setAnnotation("Аннотация");
+ event.setDescription("Описание");
+ event.setCategory(category);
+ event.setLocation(location);
+ event.setInitiator(initiator);
+ event.setCreatedOn(LocalDateTime.now());
+ event.setEventDate(LocalDateTime.now().plusDays(1));
+ event.setState(EventState.PUBLISHED);
+ event.setParticipantLimit(1);
+ em.persist(event);
+
+ em.flush();
+ }
+
+ @Test
+ void mustCreateRequest() {
+ ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId());
+
+ assertThat(requestDto.getId()).isNotNull();
+ assertThat(requestDto.getEvent()).isEqualTo(event.getId());
+ assertThat(requestDto.getRequester()).isEqualTo(user.getId());
+ assertThat(requestDto.getStatus()).isEqualTo(RequestStatus.PENDING);
+ }
+
+ @Test
+ void shouldThrowConflictWhenUserIsInitiator() {
+ assertThrows(ConflictException.class,
+ () -> requestService.createRequest(initiator.getId(), event.getId()));
+ }
+
+ @Test
+ void shouldThrowConflictWhenEventNotPublished() {
+ event.setState(EventState.PENDING);
+ em.flush();
+
+ assertThrows(ConflictException.class,
+ () -> requestService.createRequest(user.getId(), event.getId()));
+ }
+
+ @Test
+ void shouldThrowConflictWhenDuplicateRequest() {
+ requestService.createRequest(user.getId(), event.getId());
+
+ assertThrows(ConflictException.class,
+ () -> requestService.createRequest(user.getId(), event.getId()));
+ }
+
+ @Test
+ void mustReturnRequestsByUserId() {
+ ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId());
+
+ List requests = requestService.getRequestsByUserId(user.getId());
+
+ assertThat(requests).hasSize(1);
+ assertThat(requests.get(0).getId()).isEqualTo(requestDto.getId());
+ }
+
+ @Test
+ void mustCancelRequest() {
+ ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId());
+
+ ParticipationRequestDto canceled = requestService.cancelRequest(user.getId(), requestDto.getId());
+
+ assertThat(canceled.getStatus()).isEqualTo(RequestStatus.CANCELED);
+ }
+
+ @Test
+ void shouldThrowNotFoundWhenCancelingOtherUserRequest() {
+ ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId());
+
+ assertThrows(NotFoundException.class,
+ () -> requestService.cancelRequest(initiator.getId(), requestDto.getId()));
+ }
+}
+
diff --git a/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java b/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java
new file mode 100644
index 0000000..2198ef4
--- /dev/null
+++ b/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java
@@ -0,0 +1,96 @@
+package ru.practicum.user;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import ru.practicum.error.ErrorHandler;
+import ru.practicum.user.controller.UserAdminController;
+import ru.practicum.user.dto.NewUserRequest;
+import ru.practicum.user.dto.UserDto;
+import ru.practicum.user.service.UserService;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@ExtendWith(MockitoExtension.class)
+class UserAdminControllerTest {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Mock
+ private UserService userService;
+
+ @InjectMocks
+ private UserAdminController userAdminController;
+
+ private MockMvc mvc;
+
+ private UserDto userDto;
+ private NewUserRequest newUserRequest;
+
+ @BeforeEach
+ void setUp() {
+ mapper.findAndRegisterModules();
+ mvc = MockMvcBuilders
+ .standaloneSetup(userAdminController)
+ .setControllerAdvice(new ErrorHandler())
+ .build();
+
+ userDto = new UserDto(1L, "Max@mail.com", "Макс");
+ newUserRequest = new NewUserRequest("Max@mail.com", "Макс");
+ }
+
+ @Test
+ void mustGetUsers() throws Exception {
+ when(userService.getUsers(null, 0, 10))
+ .thenReturn(List.of(userDto));
+
+ mvc.perform(get("/admin/users")
+ .param("from", "0")
+ .param("size", "10")
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].id", is(1)))
+ .andExpect(jsonPath("$[0].email", is("Max@mail.com")))
+ .andExpect(jsonPath("$[0].name", is("Макс")));
+ }
+
+ @Test
+ void mustCreateUser() throws Exception {
+ when(userService.createUser(any()))
+ .thenReturn(userDto);
+
+ mvc.perform(post("/admin/users")
+ .content(mapper.writeValueAsString(newUserRequest))
+ .characterEncoding(StandardCharsets.UTF_8)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.email", is("Max@mail.com")))
+ .andExpect(jsonPath("$.name", is("Макс")));
+ }
+
+ @Test
+ void mustDeleteUser() throws Exception {
+ mvc.perform(delete("/admin/users/1"))
+ .andExpect(status().isNoContent());
+
+ verify(userService).deleteUser(1L);
+ }
+}
diff --git a/main/src/test/java/ru/practicum/user/UserDtoTest.java b/main/src/test/java/ru/practicum/user/UserDtoTest.java
new file mode 100644
index 0000000..f07959b
--- /dev/null
+++ b/main/src/test/java/ru/practicum/user/UserDtoTest.java
@@ -0,0 +1,56 @@
+package ru.practicum.user;
+
+import lombok.RequiredArgsConstructor;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.json.JsonTest;
+import org.springframework.boot.test.json.JacksonTester;
+import org.springframework.boot.test.json.JsonContent;
+import org.springframework.test.context.ContextConfiguration;
+import ru.practicum.MainServiceApp;
+import ru.practicum.user.dto.NewUserRequest;
+import ru.practicum.user.dto.UserDto;
+import ru.practicum.user.dto.UserShortDto;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+@ContextConfiguration(classes = MainServiceApp.class)
+@JsonTest
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+class UserDtoTest {
+
+ private final JacksonTester jsonNewUserRequest;
+ private final JacksonTester jsonUserDto;
+ private final JacksonTester jsonUserShortDto;
+
+ @Test
+ void shouldReturnNewUserRequest() throws Exception {
+ NewUserRequest dto = new NewUserRequest("Max@mail.com", "Макс");
+
+ JsonContent result = jsonNewUserRequest.write(dto);
+
+ assertThat(result).extractingJsonPathStringValue("$.email").isEqualTo("Max@mail.com");
+ assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Макс");
+ }
+
+ @Test
+ void shouldReturnUserDto() throws Exception {
+ UserDto dto = new UserDto(1L, "Max@mail.com", "Макс");
+
+ JsonContent result = jsonUserDto.write(dto);
+
+ assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1);
+ assertThat(result).extractingJsonPathStringValue("$.email").isEqualTo("Max@mail.com");
+ assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Макс");
+ }
+
+ @Test
+ void shouldReturnUserShortDto() throws Exception {
+ UserShortDto dto = new UserShortDto(2L, "Макс");
+
+ JsonContent result = jsonUserShortDto.write(dto);
+
+ assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(2);
+ assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Макс");
+ }
+}
diff --git a/main/src/test/java/ru/practicum/user/UserMapperTest.java b/main/src/test/java/ru/practicum/user/UserMapperTest.java
new file mode 100644
index 0000000..c5fb454
--- /dev/null
+++ b/main/src/test/java/ru/practicum/user/UserMapperTest.java
@@ -0,0 +1,44 @@
+package ru.practicum.user;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.Test;
+import ru.practicum.user.dto.NewUserRequest;
+import ru.practicum.user.dto.UserDto;
+import ru.practicum.user.dto.UserShortDto;
+import ru.practicum.user.dto.mapper.UserMapper;
+import ru.practicum.user.model.User;
+
+class UserMapperTest {
+
+ @Test
+ void shouldMapUserToUserDto() {
+ User user = new User(1L, "Max@mail.com", "Макс");
+
+ UserDto result = UserMapper.toUserDto(user);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L);
+ AssertionsForClassTypes.assertThat(result.getEmail()).isEqualTo("Max@mail.com");
+ AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс");
+ }
+
+ @Test
+ void shouldMapNewUserRequestToUser() {
+ NewUserRequest newUserRequest = new NewUserRequest("Max@mail.com", "Макс");
+
+ User result = UserMapper.toUser(newUserRequest);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isNull();
+ AssertionsForClassTypes.assertThat(result.getEmail()).isEqualTo("Max@mail.com");
+ AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс");
+ }
+
+ @Test
+ void shouldMapUserToUserShortDto() {
+ User user = new User(2L, "Max@mail.com", "Макс");
+
+ UserShortDto result = UserMapper.toUserShortDto(user);
+
+ AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(2L);
+ AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс");
+ }
+}
\ No newline at end of file
diff --git a/main/src/test/java/ru/practicum/user/UserServiceImplTest.java b/main/src/test/java/ru/practicum/user/UserServiceImplTest.java
new file mode 100644
index 0000000..b06caba
--- /dev/null
+++ b/main/src/test/java/ru/practicum/user/UserServiceImplTest.java
@@ -0,0 +1,99 @@
+package ru.practicum.user;
+
+import jakarta.persistence.EntityManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.MainServiceApp;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+import ru.practicum.user.dto.NewUserRequest;
+import ru.practicum.user.dto.UserDto;
+import ru.practicum.user.model.User;
+import ru.practicum.user.service.UserService;
+
+import java.util.List;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@Transactional
+@ActiveProfiles("test")
+@SpringBootTest(
+ classes = MainServiceApp.class,
+ webEnvironment = SpringBootTest.WebEnvironment.NONE)
+class UserServiceImplTest {
+
+ @Autowired
+ private EntityManager em;
+
+ @Autowired
+ private UserService userService;
+
+ private Long userId;
+
+ @BeforeEach
+ void setUp() {
+ User user = new User();
+ user.setEmail("Max@mail.com");
+ user.setName("Макс");
+
+ em.persist(user);
+ em.flush();
+
+ userId = user.getId();
+ }
+
+ @Test
+ void mustCreateUser() {
+ NewUserRequest newUser = new NewUserRequest("Petr@mail.com", "Петр");
+
+ UserDto created = userService.createUser(newUser);
+
+ assertThat(created.getId()).isNotNull();
+ assertThat(created.getEmail()).isEqualTo("Petr@mail.com");
+ assertThat(created.getName()).isEqualTo("Петр");
+ }
+
+ @Test
+ void mustThrowConflictExceptionWhenCreatingDuplicateUser() {
+ NewUserRequest duplicate = new NewUserRequest("Max@mail.com", "Макс");
+
+ assertThrows(ConflictException.class, () -> userService.createUser(duplicate));
+ }
+
+ @Test
+ void mustReturnUsers() {
+ List users = userService.getUsers(null, 0, 10);
+
+ assertThat(users.get(0).getId()).isEqualTo(userId);
+ assertThat(users.get(0).getEmail()).isEqualTo("Max@mail.com");
+ }
+
+ @Test
+ void mustDeleteUser() {
+ userService.deleteUser(userId);
+
+ assertThrows(NotFoundException.class, () -> userService.deleteUser(userId));
+ }
+
+ @Test
+ void shouldThrowNotFoundExceptionWhenDeletingNonExistingUser() {
+ assertThrows(NotFoundException.class, () -> userService.deleteUser(10000L));
+ }
+
+ @Test
+ void mustReturnPaginatedUsers() {
+ User u2 = new User();
+ u2.setEmail("second@mail.com");
+ u2.setName("Евгений");
+ em.persist(u2);
+ em.flush();
+
+ List users = userService.getUsers(null, 0, 1);
+ assertThat(users.get(0).getEmail()).isEqualTo("Max@mail.com");
+ }
+}
diff --git a/main/src/test/java/ru/practicum/user/UserTest.java b/main/src/test/java/ru/practicum/user/UserTest.java
new file mode 100644
index 0000000..c9c482f
--- /dev/null
+++ b/main/src/test/java/ru/practicum/user/UserTest.java
@@ -0,0 +1,28 @@
+package ru.practicum.user;
+
+import org.junit.jupiter.api.Test;
+import ru.practicum.user.model.User;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+class UserTest {
+
+ @Test
+ void testEqualsAndHashCode() {
+ User user1 = new User(1L, "test@mail.com", "Макс");
+ User user2 = new User(2L, "test@mail.com", "Петр");
+ User user3 = new User(3L, "other@mail.com", "Макс");
+
+ assertThat(user1).isEqualTo(user1);
+ assertThat(user1.hashCode()).isEqualTo(user1.hashCode());
+
+ assertThat(user1).isEqualTo(user2);
+ assertThat(user1.hashCode()).isEqualTo(user2.hashCode());
+
+ assertThat(user1).isNotEqualTo(user3);
+ assertThat(user1.hashCode()).isNotEqualTo(user3.hashCode());
+
+ assertThat(user1).isNotEqualTo(null);
+ assertThat(user1).isNotEqualTo("string");
+ }
+}
diff --git a/stats/client/pom.xml b/stats/client/pom.xml
index 3ef2b52..b947d24 100644
--- a/stats/client/pom.xml
+++ b/stats/client/pom.xml
@@ -35,5 +35,11 @@
org.springframework.boot
spring-boot-starter-actuator
+
+
+ org.projectlombok
+ lombok
+ true
+
\ No newline at end of file
diff --git a/stats/client/src/main/java/ru/practicum/StatsClient.java b/stats/client/src/main/java/ru/practicum/StatsClient.java
index 96c7c34..4f6b564 100644
--- a/stats/client/src/main/java/ru/practicum/StatsClient.java
+++ b/stats/client/src/main/java/ru/practicum/StatsClient.java
@@ -3,17 +3,23 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.util.DefaultUriBuilderFactory;
-import java.util.HashMap;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
@Service
public class StatsClient extends BaseClient {
+ private String uri;
+
@Autowired
public StatsClient(@Value("${client.url}") String serviceUrl, RestTemplateBuilder builder) {
super(
@@ -22,21 +28,37 @@ public StatsClient(@Value("${client.url}") String serviceUrl, RestTemplateBuilde
.requestFactory(() -> new HttpComponentsClientHttpRequestFactory())
.build()
);
+ uri = serviceUrl;
}
public ResponseEntity