diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..45dada4
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1 @@
+Комментарии в Pull Request обязательно должны быть на русском языке!
diff --git a/.gitignore b/.gitignore
index 64703cb..744eca1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@ target/
*.iws
*.iml
*.ipr
+/.run/
diff --git a/.run/Deployment compose-local.run.xml b/.run/Deployment compose-local.run.xml
new file mode 100644
index 0000000..4f2962b
--- /dev/null
+++ b/.run/Deployment compose-local.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/MainApplication (local).run.xml b/.run/MainApplication (local).run.xml
new file mode 100644
index 0000000..bd3928c
--- /dev/null
+++ b/.run/MainApplication (local).run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/MainApplication (docker).run.xml b/.run/MainApplication.run.xml
similarity index 53%
rename from .run/MainApplication (docker).run.xml
rename to .run/MainApplication.run.xml
index 6595b73..d0e71e8 100644
--- a/.run/MainApplication (docker).run.xml
+++ b/.run/MainApplication.run.xml
@@ -1,9 +1,8 @@
-
-
-
+
+
diff --git a/.run/StatApplication (docker).run.xml b/.run/StatApplication (docker).run.xml
deleted file mode 100644
index b2b26b0..0000000
--- a/.run/StatApplication (docker).run.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.run/StatApplication (local).run.xml b/.run/StatApplication (local).run.xml
new file mode 100644
index 0000000..ed683b0
--- /dev/null
+++ b/.run/StatApplication (local).run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/StatApplication.run.xml b/.run/StatApplication.run.xml
index ba300a8..65d72ff 100644
--- a/.run/StatApplication.run.xml
+++ b/.run/StatApplication.run.xml
@@ -1,12 +1,11 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/compose-local.yaml b/compose-local.yaml
new file mode 100644
index 0000000..33f6f10
--- /dev/null
+++ b/compose-local.yaml
@@ -0,0 +1,31 @@
+name: java-explore-with-me-local
+services:
+ stats-db-local:
+ image: postgres:16.1
+ container_name: stats_db_local
+ ports:
+ - "6542:5432"
+ environment:
+ - POSTGRES_DB=statDB_local
+ - POSTGRES_USER=localuser
+ - POSTGRES_PASSWORD=localpass
+ healthcheck:
+ test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER
+ timeout: 5s
+ interval: 5s
+ retries: 10
+
+ main-db-local:
+ image: postgres:16.1
+ container_name: main_db_local
+ ports:
+ - "6543:5432"
+ environment:
+ - POSTGRES_DB=mainDB_local
+ - POSTGRES_USER=localuser
+ - POSTGRES_PASSWORD=localpass
+ healthcheck:
+ test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER
+ timeout: 5s
+ interval: 5s
+ retries: 10
\ No newline at end of file
diff --git a/compose.yaml b/compose.yaml
index 215df4e..07cc69d 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,3 +1,4 @@
+name: java-explore-with-me
services:
stats-db:
image: postgres:16.1
@@ -14,13 +15,13 @@ services:
interval: 5s
retries: 10
- main-db:
+ ewm-db:
image: postgres:16.1
- container_name: main_db
+ container_name: ewm_db
ports:
- - "6542:5432"
+ - "5433:5432"
environment:
- - POSTGRES_DB=mainDB
+ - POSTGRES_DB=ewmDB
- POSTGRES_USER=dbuser
- POSTGRES_PASSWORD=12345
healthcheck:
@@ -41,14 +42,16 @@ services:
- SPRING_DATASOURCE_USERNAME=dbuser
- SPRING_DATASOURCE_PASSWORD=12345
- main-service:
+ ewm-service:
build: ./main-service
- container_name: main_service
+ container_name: ewm_service
ports:
- "8080:8080"
depends_on:
- - main-db
+ - stats-server
+ - ewm-db
environment:
- - SPRING_DATASOURCE_URL=jdbc:postgresql://main-db:5432/mainDB
+ - SPRING_DATASOURCE_URL=jdbc:postgresql://ewm-db:5432/ewmDB
- SPRING_DATASOURCE_USERNAME=dbuser
- SPRING_DATASOURCE_PASSWORD=12345
+ - STATS_SERVER_URL=http://stats-server:9090
\ No newline at end of file
diff --git a/main-service/pom.xml b/main-service/pom.xml
index 7335124..05cdb5c 100644
--- a/main-service/pom.xml
+++ b/main-service/pom.xml
@@ -15,50 +15,48 @@
Main Service
+
+ ru.practicum
+ stats-client
+ 0.0.1-SNAPSHOT
+ compile
+
org.springframework.boot
spring-boot-starter-web
-
org.springframework.boot
spring-boot-starter-actuator
-
org.springframework.boot
spring-boot-starter-validation
-
org.postgresql
postgresql
runtime
-
-
- ru.practicum
- stats-client
- 0.0.1-SNAPSHOT
- compile
-
-
com.h2database
h2
runtime
-
org.projectlombok
lombok
provided
-
org.springframework.boot
spring-boot-starter-data-jpa
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
@@ -67,6 +65,10 @@
org.springframework.boot
spring-boot-maven-plugin
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
diff --git a/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java b/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java
new file mode 100644
index 0000000..859fbf6
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java
@@ -0,0 +1,40 @@
+package ru.practicum.category.controller;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Positive;
+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
+@RestController
+@RequestMapping("/admin/categories")
+@RequiredArgsConstructor
+public class AdminCategoryController {
+ private final CategoryService categoryService;
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public CategoryDto createCategory(@RequestBody @Valid NewCategoryDto newCategoryDto) {
+ log.debug("Controller: createCategory data {}", newCategoryDto);
+ return categoryService.createCategory(newCategoryDto);
+ }
+
+ @DeleteMapping("/{categoryId}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void deleteCategory(@PathVariable @Positive Long categoryId) {
+ log.debug("Controller: deleteCategory id={}", categoryId);
+ categoryService.deleteCategory(categoryId);
+ }
+
+ @PatchMapping("/{categoryId}")
+ public CategoryDto updateCategory(@PathVariable @Positive Long categoryId,
+ @RequestBody @Valid NewCategoryDto newCategoryDto) {
+ log.debug("Controller: updateCategory id={}, data={}", categoryId, newCategoryDto);
+ return categoryService.updateCategory(categoryId, newCategoryDto);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java b/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java
new file mode 100644
index 0000000..367393b
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java
@@ -0,0 +1,37 @@
+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.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.service.CategoryService;
+
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/categories")
+@RequiredArgsConstructor
+public class PublicCategoryController {
+ private final CategoryService categoryService;
+
+ @GetMapping
+ public List getCategories(
+ @RequestParam(defaultValue = "0") @PositiveOrZero Integer from,
+ @RequestParam(defaultValue = "10") @Positive Integer size) {
+ int page = from / size;
+ Pageable pageable = PageRequest.of(page, size);
+ log.debug("Controller: getCategories with from={}, size={}", from, size);
+ return categoryService.getCategories(pageable);
+ }
+
+ @GetMapping("/{categoryId}")
+ public CategoryDto getCategoryById(@PathVariable @Positive Long categoryId) {
+ log.debug("Controller: getCategoryById with id={}", categoryId);
+ return categoryService.getCategoryById(categoryId);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/category/dto/CategoryDto.java b/main-service/src/main/java/ru/practicum/category/dto/CategoryDto.java
new file mode 100644
index 0000000..d5e735b
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/dto/CategoryDto.java
@@ -0,0 +1,13 @@
+package ru.practicum.category.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+public record CategoryDto(
+ Long id,
+
+ @NotBlank(message = "Название категории не может быть пустым")
+ @Size(min = 1, max = 50, message = "Название категории должно содержать от {min} до {max} символов")
+ String name
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/category/dto/NewCategoryDto.java b/main-service/src/main/java/ru/practicum/category/dto/NewCategoryDto.java
new file mode 100644
index 0000000..a57e7e5
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/dto/NewCategoryDto.java
@@ -0,0 +1,11 @@
+package ru.practicum.category.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+public record NewCategoryDto(
+ @NotBlank(message = "Название категории не может быть пустым")
+ @Size(min = 1, max = 50, message = "Название категории должно содержать от {min} до {max} символов")
+ String name
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/category/mapper/CategoryMapper.java b/main-service/src/main/java/ru/practicum/category/mapper/CategoryMapper.java
new file mode 100644
index 0000000..c8682f9
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/mapper/CategoryMapper.java
@@ -0,0 +1,22 @@
+package ru.practicum.category.mapper;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.MappingTarget;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+import ru.practicum.category.model.Category;
+
+@Mapper(componentModel = "spring")
+public interface CategoryMapper {
+
+ @Mapping(target = "id", ignore = true)
+ Category toEntity(NewCategoryDto newCategoryDto);
+
+ CategoryDto toDto(Category category);
+
+ @Mapping(target = "id", ignore = true)
+ void updateCategoryFromDto(NewCategoryDto dto, @MappingTarget Category category);
+
+ Category toEntityFromDto(CategoryDto dto);
+}
diff --git a/main-service/src/main/java/ru/practicum/category/model/Category.java b/main-service/src/main/java/ru/practicum/category/model/Category.java
new file mode 100644
index 0000000..5daf678
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/model/Category.java
@@ -0,0 +1,23 @@
+package ru.practicum.category.model;
+
+import jakarta.persistence.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+@Entity
+@Table(name = "categories")
+@Getter
+@Setter
+@ToString
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class Category {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
+ private Long id;
+
+ @Column(name = "name", unique = true, nullable = false, length = 50)
+ private String name;
+}
diff --git a/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java b/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java
new file mode 100644
index 0000000..96920b0
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java
@@ -0,0 +1,14 @@
+package ru.practicum.category.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.query.Param;
+import ru.practicum.category.model.Category;
+
+import java.util.Optional;
+
+public interface CategoryRepository extends JpaRepository {
+
+ Optional findByName(@Param("name") String name);
+
+ Optional findByNameAndIdNot(@Param("name") String name, @Param("id") Long id);
+}
diff --git a/main-service/src/main/java/ru/practicum/category/service/CategoryService.java b/main-service/src/main/java/ru/practicum/category/service/CategoryService.java
new file mode 100644
index 0000000..292cfb5
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/service/CategoryService.java
@@ -0,0 +1,22 @@
+package ru.practicum.category.service;
+
+import org.springframework.data.domain.Pageable;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.dto.NewCategoryDto;
+import ru.practicum.category.model.Category;
+
+import java.util.List;
+
+public interface CategoryService {
+ CategoryDto createCategory(NewCategoryDto newCategoryDto);
+
+ void deleteCategory(Long categoryId);
+
+ CategoryDto updateCategory(Long categoryId, NewCategoryDto newCategoryDto);
+
+ List getCategories(Pageable pageable);
+
+ CategoryDto getCategoryById(Long categoryId);
+
+ Category getCategoryByIdOrThrow(Long categoryId);
+}
diff --git a/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java b/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java
new file mode 100644
index 0000000..8f12c67
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java
@@ -0,0 +1,107 @@
+package ru.practicum.category.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+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.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.Optional;
+
+@Slf4j
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class CategoryServiceImpl implements CategoryService {
+ private final CategoryRepository categoryRepository;
+ private final CategoryMapper categoryMapper;
+ private final EventRepository eventRepository;
+
+ @Override
+ @Transactional
+ public CategoryDto createCategory(NewCategoryDto newCategoryDto) {
+ checkCategoryNameUnique(newCategoryDto.name(), null);
+
+ Category category = categoryMapper.toEntity(newCategoryDto);
+
+ Category savedCategory = categoryRepository.save(category);
+
+ log.info("Создана категория: {}", savedCategory);
+ return categoryMapper.toDto(savedCategory);
+ }
+
+ @Override
+ @Transactional
+ public void deleteCategory(Long categoryId) {
+ Category category = getCategoryByIdOrThrow(categoryId);
+
+ if (eventRepository.existsByCategoryId(categoryId)) {
+ throw new ConflictException("Невозможно удалить категорию с существующими событиями");
+ }
+
+ categoryRepository.delete(category);
+ log.info("Удалена категория с ID: {}", categoryId);
+ }
+
+ @Override
+ @Transactional
+ public CategoryDto updateCategory(Long categoryId, NewCategoryDto newCategoryDto) {
+ Category category = getCategoryByIdOrThrow(categoryId);
+
+ checkCategoryNameUnique(newCategoryDto.name(), categoryId);
+
+ categoryMapper.updateCategoryFromDto(newCategoryDto, category);
+ Category updatedCategory = categoryRepository.save(category);
+
+ log.info("Обновлена категория: {}", updatedCategory);
+ return categoryMapper.toDto(updatedCategory);
+ }
+
+ @Override
+ public List getCategories(Pageable pageable) {
+ Page categoriesPage = categoryRepository.findAll(pageable);
+
+ if (categoriesPage.isEmpty()) {
+ return List.of();
+ }
+
+ List result = categoriesPage.stream()
+ .map(categoryMapper::toDto)
+ .toList();
+
+ log.info("Найдено {} категорий", result.size());
+ return result;
+ }
+
+ @Override
+ public CategoryDto getCategoryById(Long categoryId) {
+ Category category = getCategoryByIdOrThrow(categoryId);
+ return categoryMapper.toDto(category);
+ }
+
+ @Override
+ public Category getCategoryByIdOrThrow(Long categoryId) {
+ return categoryRepository.findById(categoryId)
+ .orElseThrow(() -> new NotFoundException("Категория с id " + categoryId + " не найдена"));
+ }
+
+ private void checkCategoryNameUnique(String name, Long excludedId) {
+ Optional existingCategory = (excludedId == null)
+ ? categoryRepository.findByName(name)
+ : categoryRepository.findByNameAndIdNot(name, excludedId);
+
+ existingCategory.ifPresent(category -> {
+ throw new ConflictException("Категория с названием '" + name + "' уже существует");
+ });
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/controller/AdminCompilationController.java b/main-service/src/main/java/ru/practicum/compilation/controller/AdminCompilationController.java
new file mode 100644
index 0000000..f4c8f98
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/controller/AdminCompilationController.java
@@ -0,0 +1,41 @@
+package ru.practicum.compilation.controller;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Positive;
+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
+@RestController
+@RequestMapping("/admin/compilations")
+@RequiredArgsConstructor
+public class AdminCompilationController {
+ private final CompilationService compilationService;
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public CompilationDto createCompilation(@RequestBody @Valid NewCompilationDto request) {
+ log.debug("Controller: createCompilation data {}", request);
+ return compilationService.createCompilation(request);
+ }
+
+ @DeleteMapping("/{compId}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void deleteCompilation(@PathVariable @Positive Long compId) {
+ log.debug("Controller: deleteCompilation compId={}", compId);
+ compilationService.deleteCompilation(compId);
+ }
+
+ @PatchMapping("/{compId}")
+ public CompilationDto updateCompilation(@PathVariable @Positive Long compId,
+ @RequestBody @Valid UpdateCompilationRequest request) {
+ log.debug("Controller: updateCompilation compId={}, data {}", compId, request);
+ return compilationService.updateCompilation(compId, request);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/controller/PublicCompilationController.java b/main-service/src/main/java/ru/practicum/compilation/controller/PublicCompilationController.java
new file mode 100644
index 0000000..5ee2a3c
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/controller/PublicCompilationController.java
@@ -0,0 +1,36 @@
+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.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.compilation.dto.CompilationDto;
+import ru.practicum.compilation.service.CompilationService;
+
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/compilations")
+@RequiredArgsConstructor
+public class PublicCompilationController {
+ private final CompilationService compilationService;
+
+ @GetMapping
+ public List getCompilations(@RequestParam(required = false) Boolean pinned,
+ @RequestParam(defaultValue = "0") @PositiveOrZero Integer from,
+ @RequestParam(defaultValue = "10") @Positive Integer size) {
+ Pageable pageable = PageRequest.of(from / size, size);
+ log.debug("Controller: getCompilations pinned={}, from={}, size={}", pinned, from, size);
+ return compilationService.getCompilations(pinned, pageable);
+ }
+
+ @GetMapping("/{compId}")
+ public CompilationDto getCompilation(@PathVariable @Positive Long compId) {
+ log.debug("Controller: getCompilation compId={}", compId);
+ return compilationService.getCompilationById(compId);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/dto/CompilationDto.java b/main-service/src/main/java/ru/practicum/compilation/dto/CompilationDto.java
new file mode 100644
index 0000000..078f753
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/dto/CompilationDto.java
@@ -0,0 +1,13 @@
+package ru.practicum.compilation.dto;
+
+import ru.practicum.event.dto.EventShortDto;
+
+import java.util.Set;
+
+public record CompilationDto(
+ Long id,
+ Set events,
+ boolean pinned,
+ String title
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java b/main-service/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java
new file mode 100644
index 0000000..9348e54
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java
@@ -0,0 +1,17 @@
+package ru.practicum.compilation.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+import java.util.Set;
+
+public record NewCompilationDto(
+ Set events,
+
+ boolean pinned,
+
+ @NotBlank(message = "Заголовок подборки не может быть пустым")
+ @Size(min = 1, max = 50, message = "Заголовок должен содержать от {min} до {max} символов")
+ String title
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java b/main-service/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java
new file mode 100644
index 0000000..b7ad807
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java
@@ -0,0 +1,15 @@
+package ru.practicum.compilation.dto;
+
+import jakarta.validation.constraints.Size;
+
+import java.util.Set;
+
+public record UpdateCompilationRequest(
+ Set events,
+
+ Boolean pinned,
+
+ @Size(min = 1, max = 50, message = "Заголовок должен содержать от {min} до {max} символов")
+ String title
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/mapper/CompilationMapper.java b/main-service/src/main/java/ru/practicum/compilation/mapper/CompilationMapper.java
new file mode 100644
index 0000000..bcf2693
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/mapper/CompilationMapper.java
@@ -0,0 +1,24 @@
+package ru.practicum.compilation.mapper;
+
+import org.mapstruct.*;
+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.event.mapper.EventMapper;
+
+@Mapper(componentModel = "spring", uses = EventMapper.class)
+public interface CompilationMapper {
+
+ @Mapping(target = "id", ignore = true)
+ @Mapping(target = "events", ignore = true)
+ Compilation toEntity(NewCompilationDto request);
+
+ @Mapping(target = "events", qualifiedByName = "toEventShortWithoutStats")
+ CompilationDto toDto(Compilation compilation);
+
+ @Mapping(target = "id", ignore = true)
+ @Mapping(target = "events", ignore = true)
+ @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
+ void updateCompilationFromRequest(UpdateCompilationRequest request, @MappingTarget Compilation compilation);
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/model/Compilation.java b/main-service/src/main/java/ru/practicum/compilation/model/Compilation.java
new file mode 100644
index 0000000..d142911
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/model/Compilation.java
@@ -0,0 +1,35 @@
+package ru.practicum.compilation.model;
+
+import jakarta.persistence.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import ru.practicum.event.model.Event;
+
+import java.util.Set;
+
+@Entity
+@Table(name = "compilations")
+@Getter
+@Setter
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class Compilation {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
+ private Long id;
+
+ @Column(nullable = false, length = 50)
+ private String title;
+
+ @Column(nullable = false)
+ private boolean pinned;
+
+ @ManyToMany(fetch = FetchType.LAZY)
+ @JoinTable(
+ name = "compilation_events",
+ joinColumns = @JoinColumn(name = "compilation_id"),
+ inverseJoinColumns = @JoinColumn(name = "event_id")
+ )
+ private Set events;
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java b/main-service/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java
new file mode 100644
index 0000000..349ca99
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java
@@ -0,0 +1,24 @@
+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 org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import ru.practicum.compilation.model.Compilation;
+
+import java.util.Optional;
+
+public interface CompilationRepository extends JpaRepository {
+ Page findByPinned(Boolean pinned, Pageable pageable);
+
+ @Query("""
+ SELECT c FROM Compilation c
+ LEFT JOIN FETCH c.events e
+ LEFT JOIN FETCH e.category
+ LEFT JOIN FETCH e.initiator
+ WHERE c.id = :id""")
+ Optional findByIdWithEvents(@Param("id") Long id);
+
+ boolean existsByTitle(String title);
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/service/CompilationService.java b/main-service/src/main/java/ru/practicum/compilation/service/CompilationService.java
new file mode 100644
index 0000000..7acb92c
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/service/CompilationService.java
@@ -0,0 +1,20 @@
+package ru.practicum.compilation.service;
+
+import org.springframework.data.domain.Pageable;
+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 request);
+
+ void deleteCompilation(Long compId);
+
+ CompilationDto updateCompilation(Long compId, UpdateCompilationRequest request);
+
+ List getCompilations(Boolean pinned, Pageable pageable);
+
+ CompilationDto getCompilationById(Long compId);
+}
diff --git a/main-service/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java b/main-service/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java
new file mode 100644
index 0000000..ac5ae0a
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java
@@ -0,0 +1,133 @@
+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.data.domain.Sort;
+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.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.ConflictException;
+import ru.practicum.exception.NotFoundException;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class CompilationServiceImpl implements CompilationService {
+ private final CompilationRepository compilationRepository;
+ private final EventRepository eventRepository;
+ private final CompilationMapper compilationMapper;
+
+ @Override
+ @Transactional
+ public CompilationDto createCompilation(NewCompilationDto request) {
+ if (compilationRepository.existsByTitle(request.title()))
+ throw new ConflictException("Подборка с таким названием (" + request.title() + ") уже существует");
+
+ Compilation compilation = compilationMapper.toEntity(request);
+
+ if (request.events() != null && !request.events().isEmpty()) {
+ List events = eventRepository.findAllByEventIds(new ArrayList<>(request.events()));
+
+ if (events.size() != request.events().size()) throw new NotFoundException("Не все события найдены");
+ compilation.setEvents(new HashSet<>(events));
+
+ } else compilation.setEvents(new HashSet<>());
+ Compilation savedCompilation = compilationRepository.save(compilation);
+
+ log.info("Создана подборка: {}", request);
+ return compilationMapper.toDto(savedCompilation);
+ }
+
+ @Override
+ @Transactional
+ public void deleteCompilation(Long compId) {
+ if (!compilationRepository.existsById(compId))
+ throw new NotFoundException("Подборка с идентификатором " + compId + " не найдена");
+
+ log.info("Удалена подборка с id={}", compId);
+ compilationRepository.deleteById(compId);
+ }
+
+ @Override
+ @Transactional
+ public CompilationDto updateCompilation(Long compId, UpdateCompilationRequest request) {
+ Compilation compilation = getCompilationOrThrow(compId);
+
+ if (request.title() != null
+ && !request.title().equals(compilation.getTitle())
+ && compilationRepository.existsByTitle(request.title())) {
+ throw new ConflictException("Подборка с названием \"" + request.title() + "\" уже существует");
+ }
+
+ if (request.events() != null) {
+ if (request.events().isEmpty()) {
+ compilation.setEvents(new HashSet<>());
+ } else {
+ List events = eventRepository.findAllByEventIds(new ArrayList<>(request.events()));
+ if (events.size() != request.events().size()) {
+ throw new NotFoundException("Некоторые события не найдены");
+ }
+ compilation.setEvents(new HashSet<>(events));
+ }
+ }
+
+ compilationMapper.updateCompilationFromRequest(request, compilation);
+
+ log.info("Обновлена подборка с id={}", compId);
+ return compilationMapper.toDto(compilationRepository.save(compilation));
+ }
+
+ @Override
+ public List getCompilations(Boolean pinned, Pageable pageable) {
+ Pageable sortedPageable = PageRequest.of(
+ pageable.getPageNumber(),
+ pageable.getPageSize(),
+ Sort.by("id").ascending()
+ );
+
+ Page compilationsPage;
+ if (pinned != null)
+ compilationsPage = compilationRepository.findByPinned(pinned, sortedPageable);
+ else
+ compilationsPage = compilationRepository.findAll(sortedPageable);
+
+ List compilations = compilationsPage.getContent();
+
+ if (!compilations.isEmpty()) {
+ log.info("Получен список подборок pinned={}, pageable={}", pinned, pageable);
+ return compilations.stream()
+ .map(compilationMapper::toDto)
+ .toList();
+ }
+ log.info("Список подборок пуст");
+ return List.of();
+ }
+
+ @Override
+ public CompilationDto getCompilationById(Long compId) {
+ Compilation compilation = getCompilationOrThrow(compId);
+
+ log.info("Получена подборка с id={}", compId);
+ return compilationMapper.toDto(compilation);
+ }
+
+ private Compilation getCompilationOrThrow(Long compId) {
+ return compilationRepository.findByIdWithEvents(compId)
+ .orElseThrow(() -> new NotFoundException("Подборка с идентификатором " + compId + " не найдена"));
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/constants/DateTimeConstants.java b/main-service/src/main/java/ru/practicum/constants/DateTimeConstants.java
new file mode 100644
index 0000000..276e937
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/constants/DateTimeConstants.java
@@ -0,0 +1,9 @@
+package ru.practicum.constants;
+
+public final class DateTimeConstants {
+
+ private DateTimeConstants() {
+ }
+
+ public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
+}
diff --git a/main-service/src/main/java/ru/practicum/event/controller/AdminEventController.java b/main-service/src/main/java/ru/practicum/event/controller/AdminEventController.java
new file mode 100644
index 0000000..4a3d982
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/controller/AdminEventController.java
@@ -0,0 +1,39 @@
+package ru.practicum.event.controller;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Positive;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.SearchEventAdminRequest;
+import ru.practicum.event.dto.UpdateEventAdminRequest;
+import ru.practicum.event.service.EventService;
+
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/admin/events")
+public class AdminEventController {
+ private final EventService eventService;
+
+ @GetMapping
+ public List getEventsAdmin(@ModelAttribute @Valid SearchEventAdminRequest request) {
+ int size = (request.size() != null && request.size() > 0) ? request.size() : 10;
+ int from = request.from() != null ? request.from() : 0;
+ PageRequest pageRequest = PageRequest.of(from / size, size);
+ log.debug("Controller: getEventAdmin filters {}", request);
+ return eventService.getEventsAdmin(request, pageRequest);
+ }
+
+ @PatchMapping("/{eventId}")
+ public EventFullDto updateEventAdmin(@PathVariable @Positive Long eventId,
+ @RequestBody @Valid UpdateEventAdminRequest request
+ ) {
+ log.debug("Controller: updateEventAdmin id={}, data {}", eventId, request);
+ return eventService.updateEventAdmin(eventId, request);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/controller/PrivateEventController.java b/main-service/src/main/java/ru/practicum/event/controller/PrivateEventController.java
new file mode 100644
index 0000000..7c3d58e
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/controller/PrivateEventController.java
@@ -0,0 +1,86 @@
+package ru.practicum.event.controller;
+
+import jakarta.servlet.http.HttpServletRequest;
+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.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.EventShortDto;
+import ru.practicum.event.dto.NewEventDto;
+import ru.practicum.event.dto.UpdateEventUserRequest;
+import ru.practicum.event.service.EventService;
+import ru.practicum.request.dto.EventRequestStatusUpdateRequest;
+import ru.practicum.request.dto.EventRequestStatusUpdateResult;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.service.RequestService;
+
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/users/{userId}/events")
+@RequiredArgsConstructor
+public class PrivateEventController {
+ private final EventService eventService;
+ private final RequestService requestService;
+
+ @GetMapping
+ public List getEvents(@PathVariable("userId") @Positive Long userId,
+ @RequestParam(defaultValue = "0") @PositiveOrZero Integer from,
+ @RequestParam(defaultValue = "10") @Positive Integer size
+ ) {
+ Pageable pageable = PageRequest.of(from / size, size);
+ log.debug("Controller: getEvents with id={} with pageable {}", userId, pageable);
+ return eventService.getEvents(userId, pageable);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public EventFullDto createEvent(@PathVariable("userId") @Positive Long userId,
+ @RequestBody @Valid NewEventDto newEventDto
+ ) {
+ log.debug("Controller: createEvent with id={} with data {}", userId, newEventDto);
+ return eventService.createEvent(userId, newEventDto);
+ }
+
+ @GetMapping("/{eventId}")
+ public EventFullDto getEvent(@PathVariable("userId") @Positive Long userId,
+ @PathVariable("eventId") @Positive Long eventId,
+ HttpServletRequest request
+ ) {
+ log.debug("Controller: getEvent with id={} and eventId={}", userId, eventId);
+ return eventService.getEvent(userId, eventId, request.getRemoteAddr());
+ }
+
+ @PatchMapping("/{eventId}")
+ public EventFullDto updateEvent(@PathVariable("userId") @Positive Long userId,
+ @PathVariable("eventId") @Positive Long eventId,
+ @RequestBody @Valid UpdateEventUserRequest request
+ ) {
+ log.debug("Controller: updateEvent with id={} and eventId={} with data {}", userId, eventId, request);
+ return eventService.updateEvent(userId, eventId, request);
+ }
+
+ @GetMapping("/{eventId}/requests")
+ public List getRequestsByEvent(@PathVariable("userId") @Positive Long userId,
+ @PathVariable("eventId") @Positive Long eventId
+ ) {
+ log.debug("Controller: getRequestsByEvent with id={} and eventId={}", userId, eventId);
+ return requestService.getRequestsByEvent(userId, eventId);
+ }
+
+ @PatchMapping("/{eventId}/requests")
+ public EventRequestStatusUpdateResult updateRequestStatus(@PathVariable("userId") @Positive Long userId,
+ @PathVariable("eventId") @Positive Long eventId,
+ @RequestBody EventRequestStatusUpdateRequest request
+ ) {
+ log.debug("Controller: updateRequestStatus with id={} and eventId={} with data {}", userId, eventId, request);
+ return requestService.updateRequestStatus(userId, eventId, request);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/controller/PublicEventController.java b/main-service/src/main/java/ru/practicum/event/controller/PublicEventController.java
new file mode 100644
index 0000000..ef94b27
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/controller/PublicEventController.java
@@ -0,0 +1,40 @@
+package ru.practicum.event.controller;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Positive;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.event.dto.EventFullDto;
+import ru.practicum.event.dto.EventShortDto;
+import ru.practicum.event.dto.SearchEventPublicRequest;
+import ru.practicum.event.service.EventService;
+
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/events")
+@RequiredArgsConstructor
+public class PublicEventController {
+ private final EventService eventService;
+
+ @GetMapping
+ public List getEventsPublic(@ModelAttribute @Valid SearchEventPublicRequest request,
+ HttpServletRequest httpRequest
+ ) {
+ int size = (request.size() != null && request.size() > 0) ? request.size() : 10;
+ int from = request.from() != null ? request.from() : 0;
+ PageRequest pageRequest = PageRequest.of(from / size, size);
+ log.debug("Controller: getEventsPublic with filters {}", request);
+ return eventService.getEventsPublic(request, pageRequest, httpRequest.getRemoteAddr());
+ }
+
+ @GetMapping("/{id}")
+ public EventFullDto getEventByIdPublic(@PathVariable @Positive Long id, HttpServletRequest request) {
+ log.debug("Controller: getEventByIdPublic with id={}", id);
+ return eventService.getEventByIdPublic(id, request.getRemoteAddr());
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/EventFullDto.java b/main-service/src/main/java/ru/practicum/event/dto/EventFullDto.java
new file mode 100644
index 0000000..eb00b02
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/EventFullDto.java
@@ -0,0 +1,48 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.event.model.EventState;
+import ru.practicum.user.dto.UserShortDto;
+
+import java.time.LocalDateTime;
+
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
+public record EventFullDto(
+ Long id,
+
+ String annotation,
+
+ CategoryDto category,
+
+ Long confirmedRequests,
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_PATTERN)
+ LocalDateTime createdOn,
+
+ String description,
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_PATTERN)
+ LocalDateTime eventDate,
+
+ UserShortDto initiator,
+
+ LocationDto location,
+
+ Boolean paid,
+
+ Integer participantLimit,
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_PATTERN)
+ LocalDateTime publishedOn,
+
+ Boolean requestModeration,
+
+ EventState state,
+
+ String title,
+
+ Long views
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/EventShortDto.java b/main-service/src/main/java/ru/practicum/event/dto/EventShortDto.java
new file mode 100644
index 0000000..cdbd050
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/EventShortDto.java
@@ -0,0 +1,31 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.user.dto.UserShortDto;
+
+import java.time.LocalDateTime;
+
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
+public record EventShortDto(
+ Long id,
+
+ String annotation,
+
+ CategoryDto category,
+
+ Long confirmedRequests,
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_PATTERN)
+ LocalDateTime eventDate,
+
+ UserShortDto initiator,
+
+ boolean paid,
+
+ String title,
+
+ Long views
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/EventStatistics.java b/main-service/src/main/java/ru/practicum/event/dto/EventStatistics.java
new file mode 100644
index 0000000..f380700
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/EventStatistics.java
@@ -0,0 +1,9 @@
+package ru.practicum.event.dto;
+
+import java.util.Map;
+
+public record EventStatistics(
+ Map confirmedRequests,
+ Map views
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/EventWithCountConfirmedRequests.java b/main-service/src/main/java/ru/practicum/event/dto/EventWithCountConfirmedRequests.java
new file mode 100644
index 0000000..b76bf01
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/EventWithCountConfirmedRequests.java
@@ -0,0 +1,16 @@
+package ru.practicum.event.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class EventWithCountConfirmedRequests {
+ private Long eventId;
+ private int countConfirmedRequests;
+
+ public EventWithCountConfirmedRequests(Long eventId, Long countConfirmedRequestsLong) {
+ this.eventId = eventId;
+ this.countConfirmedRequests = (countConfirmedRequestsLong == null) ? 0 : countConfirmedRequestsLong.intValue();
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/LocationDto.java b/main-service/src/main/java/ru/practicum/event/dto/LocationDto.java
new file mode 100644
index 0000000..4bc8e89
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/LocationDto.java
@@ -0,0 +1,12 @@
+package ru.practicum.event.dto;
+
+import jakarta.validation.constraints.NotNull;
+
+public record LocationDto(
+ @NotNull(message = "Широта не может быть null")
+ Float lat,
+
+ @NotNull(message = "Долгота не может быть null")
+ Float lon
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/NewEventDto.java b/main-service/src/main/java/ru/practicum/event/dto/NewEventDto.java
new file mode 100644
index 0000000..1a0b755
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/NewEventDto.java
@@ -0,0 +1,52 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import jakarta.validation.constraints.*;
+
+import java.time.LocalDateTime;
+
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
+public record NewEventDto(
+ @NotBlank(message = "Краткое описание не может быть пустым")
+ @Size(min = 20, max = 2000, message = "Аннотация должна содержать от {min} до {max} символов")
+ String annotation,
+
+ @NotNull(message = "Id категории не может быть null")
+ Long category,
+
+ @NotBlank(message = "Описание события не может быть пустым")
+ @Size(min = 20, max = 7000, message = "Описание должно содержать от {min} до {max} символов")
+ String description,
+
+ @NotNull(message = "Дата события не может быть null")
+ @Future(message = "Дата события должна быть в будущем")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_PATTERN)
+ LocalDateTime eventDate,
+
+ @NotNull(message = "Место проведения события не может быть null")
+ LocationDto location,
+
+ Boolean paid,
+
+ @PositiveOrZero(message = "Лимит участников события должен быть нулевым или больше нуля")
+ Integer participantLimit,
+
+ Boolean requestModeration,
+
+ @NotBlank(message = "Название события не может быть пустым")
+ @Size(min = 3, max = 120, message = "Заголовок должен содержать от {min} до {max} символов")
+ String title
+) {
+ public NewEventDto {
+ if (paid == null) {
+ paid = Boolean.FALSE;
+ }
+ if (participantLimit == null) {
+ participantLimit = 0;
+ }
+ if (requestModeration == null) {
+ requestModeration = Boolean.TRUE;
+ }
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/SearchEventAdminRequest.java b/main-service/src/main/java/ru/practicum/event/dto/SearchEventAdminRequest.java
new file mode 100644
index 0000000..84695ad
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/SearchEventAdminRequest.java
@@ -0,0 +1,28 @@
+package ru.practicum.event.dto;
+
+import org.springframework.format.annotation.DateTimeFormat;
+import ru.practicum.event.model.EventState;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
+public record SearchEventAdminRequest(
+ List users,
+
+ List states,
+
+ List categories,
+
+ @DateTimeFormat(pattern = DATE_TIME_PATTERN)
+ LocalDateTime rangeStart,
+
+ @DateTimeFormat(pattern = DATE_TIME_PATTERN)
+ LocalDateTime rangeEnd,
+
+ Integer from,
+
+ Integer size
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/SearchEventPublicRequest.java b/main-service/src/main/java/ru/practicum/event/dto/SearchEventPublicRequest.java
new file mode 100644
index 0000000..1298414
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/SearchEventPublicRequest.java
@@ -0,0 +1,46 @@
+package ru.practicum.event.dto;
+
+import org.springframework.format.annotation.DateTimeFormat;
+import ru.practicum.event.model.SortState;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
+public record SearchEventPublicRequest(
+ String text,
+
+ List categories,
+
+ Boolean paid,
+
+ @DateTimeFormat(pattern = DATE_TIME_PATTERN)
+ LocalDateTime rangeStart,
+
+ @DateTimeFormat(pattern = DATE_TIME_PATTERN)
+ LocalDateTime rangeEnd,
+
+ Boolean onlyAvailable,
+
+ SortState sort,
+
+ Integer from,
+
+ Integer size
+) {
+ public SearchEventPublicRequest {
+ if (onlyAvailable == null) {
+ onlyAvailable = false;
+ }
+ if (sort == null) {
+ sort = SortState.EVENT_DATE;
+ }
+ if (from == null) {
+ from = 0;
+ }
+ if (size == null) {
+ size = 10;
+ }
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java b/main-service/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java
new file mode 100644
index 0000000..ae6da00
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java
@@ -0,0 +1,40 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import jakarta.validation.constraints.Future;
+import jakarta.validation.constraints.PositiveOrZero;
+import jakarta.validation.constraints.Size;
+import ru.practicum.event.model.StateActionAdmin;
+
+import java.time.LocalDateTime;
+
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
+public record UpdateEventAdminRequest(
+ @Size(min = 20, max = 2000, message = "Недопустимое количество символов")
+ String annotation,
+
+ Long category,
+
+ @Size(min = 20, max = 7000, message = "Описание должно содержать от {min} до {max} символов")
+ String description,
+
+ @Future(message = "Дата события должна быть в будущем")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_PATTERN)
+ LocalDateTime eventDate,
+
+ LocationDto location,
+
+ Boolean paid,
+
+ @PositiveOrZero(message = "Лимит участников события должен быть нулевым или больше нуля")
+ Integer participantLimit,
+
+ Boolean requestModeration,
+
+ StateActionAdmin stateAction,
+
+ @Size(min = 3, max = 120, message = "Заголовок должен содержать от {min} до {max} символов")
+ String title
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java b/main-service/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java
new file mode 100644
index 0000000..d98ffc0
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java
@@ -0,0 +1,40 @@
+package ru.practicum.event.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import jakarta.validation.constraints.Future;
+import jakarta.validation.constraints.PositiveOrZero;
+import jakarta.validation.constraints.Size;
+import ru.practicum.event.model.StateActionUser;
+
+import java.time.LocalDateTime;
+
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
+public record UpdateEventUserRequest(
+ @Size(min = 20, max = 2000, message = "Недопустимое количество символов")
+ String annotation,
+
+ Long category,
+
+ @Size(min = 20, max = 7000, message = "Описание должно содержать от {min} до {max} символов")
+ String description,
+
+ @Future(message = "Дата события должна быть в будущем")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_PATTERN)
+ LocalDateTime eventDate,
+
+ LocationDto location,
+
+ Boolean paid,
+
+ @PositiveOrZero(message = "Лимит участников события должен быть нулевым или больше нуля")
+ Integer participantLimit,
+
+ Boolean requestModeration,
+
+ StateActionUser stateAction,
+
+ @Size(min = 3, max = 120, message = "Заголовок должен содержать от {min} до {max} символов")
+ String title
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/event/mapper/EventMapper.java b/main-service/src/main/java/ru/practicum/event/mapper/EventMapper.java
new file mode 100644
index 0000000..6ae6349
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/mapper/EventMapper.java
@@ -0,0 +1,45 @@
+package ru.practicum.event.mapper;
+
+import org.mapstruct.*;
+import ru.practicum.category.model.Category;
+import ru.practicum.event.dto.*;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.model.EventState;
+import ru.practicum.user.model.User;
+
+@Mapper(componentModel = "spring", uses = {LocationMapper.class})
+public interface EventMapper {
+
+ @Mapping(target = "id", ignore = true)
+ @Mapping(target = "publishedOn", ignore = true)
+ @Mapping(target = "createdOn", ignore = true)
+ @Mapping(target = "category", source = "category")
+ Event fromNewEvent(NewEventDto dto, User initiator, Category category, EventState state);
+
+ EventShortDto toEventShortDto(Event event, Integer confirmedRequests, Long views);
+
+ @Named("toEventShortWithoutStats")
+ @Mapping(target = "confirmedRequests", ignore = true)
+ @Mapping(target = "views", ignore = true)
+ EventShortDto toEventShortWithoutStats(Event event);
+
+ EventFullDto toEventFullDto(Event event, Integer confirmedRequests, Long views);
+
+ @Mapping(target = "id", ignore = true)
+ @Mapping(target = "initiator", ignore = true)
+ @Mapping(target = "createdOn", ignore = true)
+ @Mapping(target = "publishedOn", ignore = true)
+ @Mapping(target = "state", ignore = true)
+ @Mapping(target = "category", source = "category")
+ @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
+ void updateEventFromUserRequest(UpdateEventUserRequest request, @MappingTarget Event event, Category category);
+
+ @Mapping(target = "id", ignore = true)
+ @Mapping(target = "initiator", ignore = true)
+ @Mapping(target = "createdOn", ignore = true)
+ @Mapping(target = "publishedOn", ignore = true)
+ @Mapping(target = "state", ignore = true)
+ @Mapping(target = "category", source = "category")
+ @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
+ void updateEventFromAdminRequest(UpdateEventAdminRequest request, @MappingTarget Event event, Category category);
+}
diff --git a/main-service/src/main/java/ru/practicum/event/mapper/LocationMapper.java b/main-service/src/main/java/ru/practicum/event/mapper/LocationMapper.java
new file mode 100644
index 0000000..1aac2bf
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/mapper/LocationMapper.java
@@ -0,0 +1,12 @@
+package ru.practicum.event.mapper;
+
+import org.mapstruct.Mapper;
+import ru.practicum.event.dto.LocationDto;
+import ru.practicum.event.model.EventLocation;
+
+@Mapper(componentModel = "spring")
+public interface LocationMapper {
+ EventLocation toEventLocation(LocationDto dto);
+
+ LocationDto toDto(EventLocation entity);
+}
diff --git a/main-service/src/main/java/ru/practicum/event/model/Event.java b/main-service/src/main/java/ru/practicum/event/model/Event.java
new file mode 100644
index 0000000..112ca24
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/model/Event.java
@@ -0,0 +1,83 @@
+package ru.practicum.event.model;
+
+import jakarta.persistence.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import org.hibernate.annotations.CreationTimestamp;
+import ru.practicum.category.model.Category;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "events")
+@Getter
+@Setter
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class Event {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
+ private Long id;
+
+ @Column(nullable = false, length = 120)
+ private String title;
+
+ @Column(nullable = false, length = 2000)
+ private String annotation;
+
+ @Column(nullable = false, length = 7000)
+ private String description;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "category_id")
+ private Category category;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "initiator_id")
+ private User initiator;
+
+ @Column(name = "created_on", nullable = false)
+ @CreationTimestamp
+ private LocalDateTime createdOn;
+
+ @Column(name = "event_date", nullable = false)
+ private LocalDateTime eventDate;
+
+ @Column(name = "published_on")
+ private LocalDateTime publishedOn;
+
+ @Embedded
+ @AttributeOverrides({
+ @AttributeOverride(name = "lat", column = @Column(name = "lat")),
+ @AttributeOverride(name = "lon", column = @Column(name = "lon"))
+ })
+ private EventLocation location;
+
+ @Column(nullable = false)
+ private boolean paid;
+
+ @Column(name = "participant_limit", nullable = false)
+ private Integer participantLimit;
+
+ @Column(name = "request_moderation", nullable = false)
+ private boolean requestModeration;
+
+ @Column(nullable = false, length = 20)
+ @Enumerated(EnumType.STRING)
+ private EventState state;
+
+ public void pending() {
+ this.state = EventState.PENDING;
+ }
+
+ public void canceled() {
+ this.state = EventState.CANCELED;
+ }
+
+ public void publish() {
+ this.state = EventState.PUBLISHED;
+ this.publishedOn = LocalDateTime.now();
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/model/EventLocation.java b/main-service/src/main/java/ru/practicum/event/model/EventLocation.java
new file mode 100644
index 0000000..64f8396
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/model/EventLocation.java
@@ -0,0 +1,17 @@
+package ru.practicum.event.model;
+
+import jakarta.persistence.Embeddable;
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+@Embeddable
+@Getter
+@Setter
+public class EventLocation {
+ @NotNull(message = "Широта не может быть null")
+ private Float lat;
+
+ @NotNull(message = "Долгота не может быть null")
+ private Float lon;
+}
diff --git a/main-service/src/main/java/ru/practicum/event/model/EventState.java b/main-service/src/main/java/ru/practicum/event/model/EventState.java
new file mode 100644
index 0000000..9cfbb30
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/model/EventState.java
@@ -0,0 +1,7 @@
+package ru.practicum.event.model;
+
+public enum EventState {
+ PENDING,
+ PUBLISHED,
+ CANCELED
+}
diff --git a/main-service/src/main/java/ru/practicum/event/model/SortState.java b/main-service/src/main/java/ru/practicum/event/model/SortState.java
new file mode 100644
index 0000000..16d7cc9
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/model/SortState.java
@@ -0,0 +1,6 @@
+package ru.practicum.event.model;
+
+public enum SortState {
+ EVENT_DATE,
+ VIEWS
+}
diff --git a/main-service/src/main/java/ru/practicum/event/model/StateActionAdmin.java b/main-service/src/main/java/ru/practicum/event/model/StateActionAdmin.java
new file mode 100644
index 0000000..1306769
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/model/StateActionAdmin.java
@@ -0,0 +1,6 @@
+package ru.practicum.event.model;
+
+public enum StateActionAdmin {
+ PUBLISH_EVENT,
+ REJECT_EVENT
+}
diff --git a/main-service/src/main/java/ru/practicum/event/model/StateActionUser.java b/main-service/src/main/java/ru/practicum/event/model/StateActionUser.java
new file mode 100644
index 0000000..e629286
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/model/StateActionUser.java
@@ -0,0 +1,6 @@
+package ru.practicum.event.model;
+
+public enum StateActionUser {
+ SEND_TO_REVIEW,
+ CANCEL_REVIEW
+}
diff --git a/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java b/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java
new file mode 100644
index 0000000..7e4d094
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java
@@ -0,0 +1,37 @@
+package ru.practicum.event.repository;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import ru.practicum.event.model.Event;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface EventRepository extends JpaRepository, JpaSpecificationExecutor {
+
+ Page findAllByInitiatorIdOrderByCreatedOnDesc(Long initiatorId, Pageable pageable);
+
+ Event findFirstByOrderByCreatedOnAsc();
+
+ @Query("""
+ SELECT e FROM Event e
+ LEFT JOIN FETCH e.category
+ LEFT JOIN FETCH e.initiator
+ WHERE e.id IN :eventIds"""
+ )
+ List findAllByEventIds(@Param("eventIds") List eventIds);
+
+ @Query("""
+ SELECT e FROM Event e
+ LEFT JOIN FETCH e.category
+ LEFT JOIN FETCH e.initiator
+ WHERE e.id = :eventId"""
+ )
+ Optional findByIdNew(@Param("eventId") Long eventId);
+
+ boolean existsByCategoryId(@Param("categoryId") Long categoryId);
+}
diff --git a/main-service/src/main/java/ru/practicum/event/repository/SearchEventSpecifications.java b/main-service/src/main/java/ru/practicum/event/repository/SearchEventSpecifications.java
new file mode 100644
index 0000000..dfb6b91
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/repository/SearchEventSpecifications.java
@@ -0,0 +1,93 @@
+package ru.practicum.event.repository;
+
+import jakarta.persistence.criteria.Root;
+import jakarta.persistence.criteria.Subquery;
+import org.springframework.data.jpa.domain.Specification;
+import ru.practicum.event.model.Event;
+import ru.practicum.event.model.EventState;
+import ru.practicum.request.model.Request;
+import ru.practicum.request.model.RequestStatus;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public class SearchEventSpecifications {
+
+ public static Specification addWhereNull() {
+ return (root, query, criteriaBuilder) -> null;
+ }
+
+ public static Specification addWhereUsers(List userIds) {
+ return (root, query, criteriaBuilder) -> {
+ if (userIds == null || userIds.isEmpty()) return criteriaBuilder.conjunction();
+ return root.get("initiator").get("id").in(userIds);
+ };
+ }
+
+ public static Specification addWhereStates(List states) {
+ return (root, query, criteriaBuilder) -> {
+ if (states == null || states.isEmpty()) return criteriaBuilder.conjunction();
+ return root.get("state").in(states);
+ };
+ }
+
+ public static Specification addWhereCategories(List categoryIds) {
+ return (root, query, criteriaBuilder) -> {
+ if (categoryIds == null || categoryIds.isEmpty()) return criteriaBuilder.conjunction();
+ return root.get("category").get("id").in(categoryIds);
+ };
+ }
+
+ public static Specification addWhereStartsBefore(LocalDateTime dateTime) {
+ return (root, query, criteriaBuilder) -> {
+ if (dateTime == null) return criteriaBuilder.conjunction();
+ return criteriaBuilder.greaterThanOrEqualTo(root.get("eventDate"), dateTime);
+ };
+ }
+
+ public static Specification addWhereEndsAfter(LocalDateTime dateTime) {
+ return (root, query, criteriaBuilder) -> {
+ if (dateTime == null) return criteriaBuilder.conjunction();
+ return criteriaBuilder.lessThanOrEqualTo(root.get("eventDate"), dateTime);
+ };
+ }
+
+ public static Specification addLikeText(String text) {
+ return (root, query, criteriaBuilder) -> {
+ if (text == null || text.trim().isEmpty()) return criteriaBuilder.conjunction();
+ String searchText = "%" + text.toLowerCase() + "%";
+ return criteriaBuilder.or(
+ criteriaBuilder.like(criteriaBuilder.lower(root.get("annotation")), searchText),
+ criteriaBuilder.like(criteriaBuilder.lower(root.get("description")), searchText)
+ );
+ };
+ }
+
+ public static Specification isPaid(Boolean paid) {
+ return (root, query, criteriaBuilder) -> {
+ if (paid == null) return criteriaBuilder.conjunction();
+ return criteriaBuilder.equal(root.get("paid"), paid);
+ };
+ }
+
+ public static Specification addWhereAvailableSlots() {
+ return (root, query, criteriaBuilder) -> {
+ assert query != null;
+ Subquery subquery = query.subquery(Long.class);
+ Root requestRoot = subquery.from(Request.class);
+
+ subquery.select(criteriaBuilder.count(requestRoot));
+ subquery.where(
+ criteriaBuilder.and(
+ criteriaBuilder.equal(requestRoot.get("event"), root),
+ criteriaBuilder.equal(requestRoot.get("status"), RequestStatus.CONFIRMED)
+ )
+ );
+
+ return criteriaBuilder.or(
+ criteriaBuilder.equal(root.get("participantLimit"), 0),
+ criteriaBuilder.lessThan(subquery, root.get("participantLimit"))
+ );
+ };
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/event/service/EventService.java b/main-service/src/main/java/ru/practicum/event/service/EventService.java
new file mode 100644
index 0000000..03f9882
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/service/EventService.java
@@ -0,0 +1,27 @@
+package ru.practicum.event.service;
+
+import org.springframework.data.domain.Pageable;
+import ru.practicum.event.dto.*;
+import ru.practicum.event.model.Event;
+
+import java.util.List;
+
+public interface EventService {
+ List getEvents(Long userId, Pageable pageable);
+
+ EventFullDto createEvent(Long userId, NewEventDto newEventDto);
+
+ EventFullDto getEvent(Long userId, Long eventId, String ip);
+
+ EventFullDto updateEvent(Long userId, Long eventId, UpdateEventUserRequest request);
+
+ Event getEventOrThrow(Long eventId);
+
+ List getEventsAdmin(SearchEventAdminRequest request, Pageable pageable);
+
+ EventFullDto updateEventAdmin(Long eventId, UpdateEventAdminRequest request);
+
+ List getEventsPublic(SearchEventPublicRequest requestParams, Pageable pageable, String ip);
+
+ EventFullDto getEventByIdPublic(Long eventId, String ip);
+}
diff --git a/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java b/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java
new file mode 100644
index 0000000..8608b9a
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java
@@ -0,0 +1,362 @@
+package ru.practicum.event.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.practicum.CreateEndpointHitDto;
+import ru.practicum.StatsClient;
+import ru.practicum.ViewStatsDto;
+import ru.practicum.category.dto.CategoryDto;
+import ru.practicum.category.mapper.CategoryMapper;
+import ru.practicum.category.model.Category;
+import ru.practicum.category.service.CategoryService;
+import ru.practicum.event.dto.*;
+import ru.practicum.event.mapper.EventMapper;
+import ru.practicum.event.model.*;
+import ru.practicum.event.repository.EventRepository;
+import ru.practicum.event.repository.SearchEventSpecifications;
+import ru.practicum.exception.BadRequestException;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+import ru.practicum.exception.ValidationException;
+import ru.practicum.request.repository.RequestRepository;
+import ru.practicum.user.model.User;
+import ru.practicum.user.service.UserService;
+
+import java.time.LocalDateTime;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class EventServiceImpl implements EventService {
+ private final EventRepository eventRepository;
+ private final EventMapper eventMapper;
+ private final UserService userService;
+ private final RequestRepository requestRepository;
+ private final StatsClient statsClient;
+ private final CategoryService categoryService;
+ private final CategoryMapper categoryMapper;
+
+ @Override
+ @Transactional
+ public EventFullDto createEvent(Long userId, NewEventDto newEventDto) {
+ validateDateEvent(newEventDto.eventDate(), 2);
+
+ User user = userService.getUserById(userId);
+
+ CategoryDto categoryDto = categoryService.getCategoryById(newEventDto.category());
+ Category category = categoryMapper.toEntityFromDto(categoryDto);
+
+ Event event = eventMapper.fromNewEvent(newEventDto, user, category, EventState.PENDING);
+ event = eventRepository.save(event);
+
+ log.info("Создано событие с id={}, title={}, initiatorId={}", event.getId(), event.getTitle(), userId);
+ return eventMapper.toEventFullDto(event, 0, 0L);
+ }
+
+ @Override
+ public List getEvents(Long userId, Pageable pageable) {
+ userService.getUserById(userId);
+ Page eventsPage = eventRepository.findAllByInitiatorIdOrderByCreatedOnDesc(userId, pageable);
+
+ if (eventsPage.isEmpty()) return List.of();
+
+ List eventIds = eventsPage.stream()
+ .map(Event::getId)
+ .toList();
+
+ EventStatistics stats = getEventStatistics(eventIds);
+
+ return eventsPage.stream()
+ .map(event -> eventMapper.toEventShortDto(
+ event,
+ stats.confirmedRequests().getOrDefault(event.getId(), 0),
+ stats.views().getOrDefault(event.getId(), 0L)
+ ))
+ .toList();
+ }
+
+ @Override
+ public EventFullDto getEvent(Long userId, Long eventId, String ip) {
+ userService.getUserByIdOrThrow(userId);
+ Event event = getEventOrThrow(eventId);
+
+ if (!event.getInitiator().getId().equals(userId)) {
+ throw new NotFoundException("Событие c id " + eventId + " не найдено у пользователя с id " + userId);
+ }
+
+ saveHit("/events/" + eventId, ip);
+
+ return buildFullDto(event);
+ }
+
+ @Override
+ @Transactional
+ public EventFullDto updateEvent(Long userId, Long eventId, UpdateEventUserRequest request) {
+ userService.getUserByIdOrThrow(userId);
+ Event event = getEventOrThrow(eventId);
+
+ if (!event.getInitiator().getId().equals(userId))
+ throw new ValidationException("Пользователь не является инициатором события и не может его редактировать");
+
+ if (event.getState() == EventState.PUBLISHED)
+ throw new ValidationException("Изменять можно только не опубликованные события");
+
+ validateDateEvent(request.eventDate(), 2);
+
+ Category category = null;
+ if (request.category() != null) {
+ category = categoryService.getCategoryByIdOrThrow(request.category());
+ }
+ eventMapper.updateEventFromUserRequest(request, event, category);
+
+ if (request.stateAction() != null) {
+ if (request.stateAction() == StateActionUser.SEND_TO_REVIEW)
+ event.pending();
+ if (request.stateAction() == StateActionUser.CANCEL_REVIEW)
+ event.canceled();
+ }
+
+ Event updatedEvent = eventRepository.save(event);
+ return buildFullDto(updatedEvent);
+ }
+
+ @Override
+ public Event getEventOrThrow(Long eventId) {
+ return eventRepository.findById(eventId)
+ .orElseThrow(() -> new NotFoundException("Событие c id " + eventId + " не найдено"));
+ }
+
+ @Override
+ public List getEventsAdmin(SearchEventAdminRequest request, Pageable pageable) {
+ validateRangeStartAndEnd(request.rangeStart(), request.rangeEnd());
+
+ Specification specification = SearchEventSpecifications.addWhereNull();
+ if (request.users() != null && !request.users().isEmpty())
+ specification = specification.and(SearchEventSpecifications.addWhereUsers(request.users()));
+ if (request.states() != null && !request.states().isEmpty())
+ specification = specification.and(SearchEventSpecifications.addWhereStates(request.states()));
+ if (request.categories() != null && !request.categories().isEmpty())
+ specification = specification.and(SearchEventSpecifications.addWhereCategories(request.categories()));
+ if (request.rangeStart() != null)
+ specification = specification.and(SearchEventSpecifications.addWhereStartsBefore(request.rangeStart()));
+ if (request.rangeEnd() != null)
+ specification = specification.and(SearchEventSpecifications.addWhereEndsAfter(request.rangeEnd()));
+ if (request.rangeStart() == null && request.rangeEnd() == null)
+ specification = specification.and(SearchEventSpecifications.addWhereStartsBefore(LocalDateTime.now()));
+
+ Page eventIdsPage = eventRepository.findAll(specification, pageable).map(Event::getId);
+ List eventIds = eventIdsPage.getContent();
+
+ if (eventIds.isEmpty()) return List.of();
+ List events = eventRepository.findAllByEventIds(eventIds);
+
+ if (events.isEmpty()) return List.of();
+
+ List searchEventIds = events.stream()
+ .map(Event::getId)
+ .toList();
+
+ EventStatistics stats = getEventStatistics(searchEventIds);
+
+ return events.stream()
+ .map(event -> eventMapper.toEventFullDto(
+ event,
+ stats.confirmedRequests().getOrDefault(event.getId(), 0),
+ stats.views().getOrDefault(event.getId(), 0L)
+ ))
+ .toList();
+ }
+
+ @Override
+ @Transactional
+ public EventFullDto updateEventAdmin(Long eventId, UpdateEventAdminRequest request) {
+ Event event = eventRepository.findByIdNew(eventId)
+ .orElseThrow(() -> new NotFoundException("Событие c id " + eventId + " не найдено"));
+
+ if (request.stateAction() != null) {
+ StateActionAdmin stateActionAdmin = request.stateAction();
+ EventState currentState = event.getState();
+
+ if (stateActionAdmin == StateActionAdmin.PUBLISH_EVENT) {
+ if (currentState != EventState.PENDING)
+ throw new ConflictException("Событие можно опубликовать только если оно в состоянии ожидания публикации");
+ validateDateEvent(event.getEventDate(), 1);
+ event.publish();
+ }
+ if (stateActionAdmin == StateActionAdmin.REJECT_EVENT) {
+ if (currentState == EventState.PUBLISHED)
+ throw new ConflictException("Событие можно отклонить пока оно не опубликовано");
+ event.canceled();
+ }
+ }
+
+ Category category = null;
+ if (request.category() != null) {
+ category = categoryService.getCategoryByIdOrThrow(request.category());
+ }
+
+ eventMapper.updateEventFromAdminRequest(request, event, category);
+ Event updatedEvent = eventRepository.save(event);
+
+ log.info("Обновлено событие с id={}, title={}, initiatorId={}",
+ updatedEvent.getId(), updatedEvent.getTitle(), updatedEvent.getInitiator().getId());
+ return buildFullDto(updatedEvent);
+ }
+
+ @Override
+ public List getEventsPublic(SearchEventPublicRequest request, Pageable pageable, String ip) {
+ validateRangeStartAndEnd(request.rangeStart(), request.rangeEnd());
+
+ Specification specification = SearchEventSpecifications.addWhereNull();
+ if (request.text() != null && !request.text().trim().isEmpty())
+ specification = specification.and(SearchEventSpecifications.addLikeText(request.text()));
+ if (request.categories() != null && !request.categories().isEmpty())
+ specification = specification.and(SearchEventSpecifications.addWhereCategories(request.categories()));
+ if (request.paid() != null)
+ specification = specification.and(SearchEventSpecifications.isPaid(request.paid()));
+ LocalDateTime rangeStart = (request.rangeStart() == null && request.rangeEnd() == null) ?
+ LocalDateTime.now() : request.rangeStart();
+ if (rangeStart != null)
+ specification = specification.and(SearchEventSpecifications.addWhereStartsBefore(rangeStart));
+ if (request.rangeEnd() != null)
+ specification = specification.and(SearchEventSpecifications.addWhereEndsAfter(request.rangeEnd()));
+ if (request.onlyAvailable())
+ specification = specification.and(SearchEventSpecifications.addWhereAvailableSlots());
+
+ List events = eventRepository.findAll(specification, pageable).getContent();
+
+ if (events.isEmpty()) return List.of();
+
+ List eventIds = events.stream()
+ .map(Event::getId)
+ .toList();
+
+ EventStatistics stats = getEventStatistics(eventIds);
+
+ List result = events.stream()
+ .map(event -> eventMapper.toEventShortDto(
+ event,
+ stats.confirmedRequests().getOrDefault(event.getId(), 0),
+ stats.views().getOrDefault(event.getId(), 0L)
+ ))
+ .toList();
+
+ saveHit("/events", ip);
+
+ if (SortState.VIEWS.equals(request.sort())) {
+ return result.stream()
+ .sorted(Comparator.comparing(EventShortDto::views).reversed())
+ .toList();
+ } else if (SortState.EVENT_DATE.equals(request.sort())) {
+ return result.stream()
+ .sorted(Comparator.comparing(EventShortDto::eventDate))
+ .toList();
+ }
+
+ return result;
+ }
+
+ @Override
+ public EventFullDto getEventByIdPublic(Long eventId, String ip) {
+ Event event = eventRepository.findByIdNew(eventId)
+ .filter(ev -> ev.getState() == EventState.PUBLISHED)
+ .orElseThrow(() -> new NotFoundException("Событие c id " + eventId + " не найдено"));
+
+ saveHit("/events/" + eventId, ip);
+
+ return buildFullDto(event);
+ }
+
+ private EventFullDto buildFullDto(Event event) {
+ Map confirmedRequests = getConfirmedRequests(List.of(event.getId()));
+ Map views = getViewsForEvents(List.of(event.getId()));
+
+ return eventMapper.toEventFullDto(
+ event,
+ confirmedRequests.getOrDefault(event.getId(), 0),
+ views.getOrDefault(event.getId(), 0L)
+ );
+ }
+
+ private void validateRangeStartAndEnd(LocalDateTime rangeStart, LocalDateTime rangeEnd) {
+ if (rangeStart != null && rangeEnd != null && rangeStart.isAfter(rangeEnd))
+ throw new BadRequestException("Дата начала не может быть позже даты окончания");
+ }
+
+ private void saveHit(String path, String ip) {
+ CreateEndpointHitDto endpointHitDto = new CreateEndpointHitDto("main-service", path, ip, LocalDateTime.now());
+ statsClient.saveHit(endpointHitDto);
+ }
+
+ private void validateDateEvent(LocalDateTime eventDate, long minHoursBeforeStartEvent) {
+ if (eventDate != null && eventDate.isBefore(LocalDateTime.now().plusHours(minHoursBeforeStartEvent)))
+ throw new ValidationException("Дата начала события не может быть ранее чем через " + minHoursBeforeStartEvent + " часа(ов)");
+ }
+
+ private Map getConfirmedRequests(List eventIds) {
+ if (eventIds.isEmpty()) return Map.of();
+
+ List events = requestRepository.findCountConfirmedRequestsByEventIds(eventIds);
+ Map confirmedRequests = eventIds.stream()
+ .collect(Collectors.toMap(id -> id, id -> 0));
+
+ events.forEach(dto -> confirmedRequests.put(dto.getEventId(), dto.getCountConfirmedRequests()));
+
+ return confirmedRequests;
+ }
+
+ private Map getViewsForEvents(List eventIds) {
+ if (eventIds.isEmpty()) return Map.of();
+
+ List uris = eventIds.stream()
+ .map(id -> "/events/" + id)
+ .toList();
+
+ LocalDateTime start = eventRepository.findFirstByOrderByCreatedOnAsc().getCreatedOn();
+ LocalDateTime end = LocalDateTime.now();
+
+ List stats = statsClient.getStats(start, end, uris, true);
+
+ Map views = eventIds.stream()
+ .collect(Collectors.toMap(id -> id, id -> 0L));
+
+ if (stats != null && !stats.isEmpty()) {
+ stats.forEach(stat -> {
+ Long eventId = getEventIdFromUri(stat.uri());
+ if (eventId > -1L) {
+ views.put(eventId, stat.hits());
+ }
+ });
+ }
+ return views;
+ }
+
+ private Long getEventIdFromUri(String uri) {
+ try {
+ return Long.parseLong(uri.substring("/events".length() + 1));
+ } catch (Exception e) {
+ return -1L;
+ }
+ }
+
+ private EventStatistics getEventStatistics(List eventIds) {
+ if (eventIds.isEmpty()) {
+ return new EventStatistics(Map.of(), Map.of());
+ }
+
+ Map confirmedRequests = getConfirmedRequests(eventIds);
+ Map views = getViewsForEvents(eventIds);
+
+ return new EventStatistics(confirmedRequests, views);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/exception/BadRequestException.java b/main-service/src/main/java/ru/practicum/exception/BadRequestException.java
new file mode 100644
index 0000000..7aee4bc
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/exception/BadRequestException.java
@@ -0,0 +1,7 @@
+package ru.practicum.exception;
+
+public class BadRequestException extends RuntimeException {
+ public BadRequestException(String message) {
+ super(message);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/exception/ConflictException.java b/main-service/src/main/java/ru/practicum/exception/ConflictException.java
new file mode 100644
index 0000000..e2fddf1
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/exception/ConflictException.java
@@ -0,0 +1,7 @@
+package ru.practicum.exception;
+
+public class ConflictException extends RuntimeException {
+ public ConflictException(String message) {
+ super(message);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/exception/ErrorHandler.java b/main-service/src/main/java/ru/practicum/exception/ErrorHandler.java
new file mode 100644
index 0000000..de60fcd
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/exception/ErrorHandler.java
@@ -0,0 +1,139 @@
+package ru.practicum.exception;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.MessageSourceResolvable;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.HttpMediaTypeNotSupportedException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.ServletRequestBindingException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.HandlerMethodValidationException;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+import java.util.stream.Collectors;
+
+@Slf4j
+@RestControllerAdvice
+public class ErrorHandler {
+
+ @ExceptionHandler(NotFoundException.class)
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public ErrorResponse handleNotFoundException(NotFoundException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.NOT_FOUND, exception, request);
+ }
+
+ @ExceptionHandler(BadRequestException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ErrorResponse handleBadRequestException(BadRequestException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.BAD_REQUEST, exception, request);
+ }
+
+ @ExceptionHandler(ForbiddenException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public ErrorResponse handleForbiddenException(ForbiddenException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.FORBIDDEN, exception, request);
+ }
+
+ @ExceptionHandler(ConflictException.class)
+ @ResponseStatus(HttpStatus.CONFLICT)
+ public ErrorResponse handleConflictException(ConflictException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.CONFLICT, exception, request);
+ }
+
+ @ExceptionHandler(ValidationException.class)
+ @ResponseStatus(HttpStatus.CONFLICT)
+ public ErrorResponse handleValidationException(ValidationException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.CONFLICT, exception, request);
+ }
+
+ @ExceptionHandler(ConstraintViolationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ErrorResponse handleConstraintViolation(ConstraintViolationException ex, HttpServletRequest request) {
+ String violations = ex.getConstraintViolations().stream()
+ .map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
+ .collect(Collectors.joining("; "));
+
+ String message = violations.isEmpty() ? "Constraint violation" : violations;
+ return buildResponse(HttpStatus.BAD_REQUEST, message, null, request);
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ErrorResponse handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpServletRequest request) {
+ String validationErrors = ex.getBindingResult().getFieldErrors().stream()
+ .map(error -> error.getField() + ": " + error.getDefaultMessage())
+ .collect(Collectors.joining("; "));
+
+ String message = validationErrors.isEmpty() ? "Validation failed" : validationErrors;
+
+ return buildResponse(HttpStatus.BAD_REQUEST, message, null, request);
+ }
+
+ @ExceptionHandler(HandlerMethodValidationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ErrorResponse handleHandlerMethodValidation(HandlerMethodValidationException ex, HttpServletRequest request) {
+ String validationErrors = ex.getAllErrors().stream()
+ .map(MessageSourceResolvable::getDefaultMessage)
+ .collect(Collectors.joining("; "));
+
+ String message = validationErrors.isEmpty() ? "Validation failed" : validationErrors;
+
+ return buildResponse(HttpStatus.BAD_REQUEST, message, null, request);
+ }
+
+ @ExceptionHandler(ServletRequestBindingException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ErrorResponse handleServletRequestBindingException(ServletRequestBindingException ex, HttpServletRequest request) {
+ return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), null, request);
+ }
+
+ @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+ @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
+ public ErrorResponse handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) {
+ return buildResponse(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage(), null, request);
+ }
+
+ @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
+ @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
+ public ErrorResponse handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, HttpServletRequest request) {
+ return buildResponse(HttpStatus.UNSUPPORTED_MEDIA_TYPE, ex.getMessage(), null, request);
+ }
+
+ @ExceptionHandler(NoHandlerFoundException.class)
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public ErrorResponse handleNoHandlerFoundException(NoHandlerFoundException ex, HttpServletRequest request) {
+ return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage(), null, request);
+ }
+
+ @ExceptionHandler(Exception.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public ErrorResponse handleUnexpected(Exception exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, exception, request);
+ }
+
+ private ErrorResponse buildResponse(HttpStatus httpStatus, Exception exception, HttpServletRequest request) {
+ return buildResponse(httpStatus, exception.getMessage(), exception, request);
+ }
+
+ private ErrorResponse buildResponse(HttpStatus httpStatus, String message, Exception exception, HttpServletRequest request) {
+ String path = request.getRequestURI();
+ String httpMethod = request.getMethod();
+ int statusCode = httpStatus.value();
+ String error = httpStatus.getReasonPhrase();
+
+ String logMessage = String.format("Path: %s %s, '%d' %s — %s", httpMethod, path, statusCode, error, message);
+
+ if (statusCode >= 500) {
+ log.error(logMessage, exception);
+ } else {
+ log.warn(logMessage);
+ }
+
+ return new ErrorResponse(path, httpMethod, statusCode, error, message);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/exception/ErrorResponse.java b/main-service/src/main/java/ru/practicum/exception/ErrorResponse.java
new file mode 100644
index 0000000..75785f0
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/exception/ErrorResponse.java
@@ -0,0 +1,10 @@
+package ru.practicum.exception;
+
+public record ErrorResponse(
+ String path,
+ String httpMethod,
+ int statusCode,
+ String error,
+ String message
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/exception/ForbiddenException.java b/main-service/src/main/java/ru/practicum/exception/ForbiddenException.java
new file mode 100644
index 0000000..85aebb9
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/exception/ForbiddenException.java
@@ -0,0 +1,7 @@
+package ru.practicum.exception;
+
+public class ForbiddenException extends RuntimeException {
+ public ForbiddenException(String message) {
+ super(message);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/exception/NotFoundException.java b/main-service/src/main/java/ru/practicum/exception/NotFoundException.java
new file mode 100644
index 0000000..628c788
--- /dev/null
+++ b/main-service/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-service/src/main/java/ru/practicum/exception/ValidationException.java b/main-service/src/main/java/ru/practicum/exception/ValidationException.java
new file mode 100644
index 0000000..d751d21
--- /dev/null
+++ b/main-service/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-service/src/main/java/ru/practicum/request/controller/PrivateRequestController.java b/main-service/src/main/java/ru/practicum/request/controller/PrivateRequestController.java
new file mode 100644
index 0000000..23ae88d
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/request/controller/PrivateRequestController.java
@@ -0,0 +1,40 @@
+package ru.practicum.request.controller;
+
+import jakarta.validation.constraints.Positive;
+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
+@RestController
+@RequestMapping("/users/{userId}/requests")
+@RequiredArgsConstructor
+public class PrivateRequestController {
+ private final RequestService requestService;
+
+ @GetMapping
+ public List getRequestsByUserId(@PathVariable("userId") @Positive Long userId) {
+ log.debug("Controller: getRequestsByUserId userId={}", userId);
+ return requestService.getRequestsByUserId(userId);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public ParticipationRequestDto addRequest(@PathVariable("userId") @Positive Long userId,
+ @RequestParam("eventId") @Positive Long eventId) {
+ log.debug("Controller: addRequest userId={}, eventId={}", userId, eventId);
+ return requestService.addRequest(userId, eventId);
+ }
+
+ @PatchMapping("/{requestId}/cancel")
+ public ParticipationRequestDto cancelRequest(@PathVariable("userId") @Positive Long userId,
+ @PathVariable("requestId") @Positive Long requestId) {
+ log.debug("Controller: cancelRequest userId={}, requestId={}", userId, requestId);
+ return requestService.cancelRequest(userId, requestId);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/request/dto/EventRequestStatusUpdateRequest.java b/main-service/src/main/java/ru/practicum/request/dto/EventRequestStatusUpdateRequest.java
new file mode 100644
index 0000000..82e2ceb
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/request/dto/EventRequestStatusUpdateRequest.java
@@ -0,0 +1,11 @@
+package ru.practicum.request.dto;
+
+import ru.practicum.request.model.RequestStatus;
+
+import java.util.List;
+
+public record EventRequestStatusUpdateRequest(
+ List requestIds,
+ RequestStatus status
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/request/dto/EventRequestStatusUpdateResult.java b/main-service/src/main/java/ru/practicum/request/dto/EventRequestStatusUpdateResult.java
new file mode 100644
index 0000000..9190900
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/request/dto/EventRequestStatusUpdateResult.java
@@ -0,0 +1,9 @@
+package ru.practicum.request.dto;
+
+import java.util.List;
+
+public record EventRequestStatusUpdateResult(
+ List confirmedRequests,
+ List rejectedRequests
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java b/main-service/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java
new file mode 100644
index 0000000..dfad229
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java
@@ -0,0 +1,22 @@
+package ru.practicum.request.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import ru.practicum.request.model.RequestStatus;
+
+import java.time.LocalDateTime;
+
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
+public record ParticipationRequestDto(
+ Long id,
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_PATTERN)
+ LocalDateTime created,
+
+ Long event,
+
+ Long requester,
+
+ RequestStatus status
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/request/mapper/RequestMapper.java b/main-service/src/main/java/ru/practicum/request/mapper/RequestMapper.java
new file mode 100644
index 0000000..be41b54
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/request/mapper/RequestMapper.java
@@ -0,0 +1,29 @@
+package ru.practicum.request.mapper;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import ru.practicum.event.model.Event;
+import ru.practicum.request.dto.EventRequestStatusUpdateResult;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.model.Request;
+import ru.practicum.request.model.RequestStatus;
+import ru.practicum.user.model.User;
+
+import java.util.List;
+
+@Mapper(componentModel = "spring")
+public interface RequestMapper {
+
+ @Mapping(target = "id", ignore = true)
+ @Mapping(target = "created", ignore = true)
+ Request toEntity(Event event, User requester, RequestStatus status);
+
+ @Mapping(source = "event.id", target = "event")
+ @Mapping(source = "requester.id", target = "requester")
+ ParticipationRequestDto toDto(Request request);
+
+ EventRequestStatusUpdateResult toEventRequestStatusUpdateResultDto(
+ List confirmedRequests,
+ List rejectedRequests
+ );
+}
\ No newline at end of file
diff --git a/main-service/src/main/java/ru/practicum/request/model/Request.java b/main-service/src/main/java/ru/practicum/request/model/Request.java
new file mode 100644
index 0000000..8d4b3ec
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/request/model/Request.java
@@ -0,0 +1,49 @@
+package ru.practicum.request.model;
+
+import jakarta.persistence.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import org.hibernate.annotations.CreationTimestamp;
+import ru.practicum.event.model.Event;
+import ru.practicum.user.model.User;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "requests")
+@Getter
+@Setter
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class Request {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "event_id")
+ private Event event;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "requester_id")
+ private User requester;
+
+ @CreationTimestamp
+ private LocalDateTime created;
+
+ @Enumerated(EnumType.STRING)
+ private RequestStatus status;
+
+ public void confirmed() {
+ this.status = RequestStatus.CONFIRMED;
+ }
+
+ public void rejected() {
+ this.status = RequestStatus.REJECTED;
+ }
+
+ public void canceled() {
+ this.status = RequestStatus.CANCELED;
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/request/model/RequestStatus.java b/main-service/src/main/java/ru/practicum/request/model/RequestStatus.java
new file mode 100644
index 0000000..af9a14f
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/request/model/RequestStatus.java
@@ -0,0 +1,8 @@
+package ru.practicum.request.model;
+
+public enum RequestStatus {
+ PENDING,
+ CONFIRMED,
+ REJECTED,
+ CANCELED
+}
diff --git a/main-service/src/main/java/ru/practicum/request/repository/RequestRepository.java b/main-service/src/main/java/ru/practicum/request/repository/RequestRepository.java
new file mode 100644
index 0000000..cda3855
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/request/repository/RequestRepository.java
@@ -0,0 +1,56 @@
+package ru.practicum.request.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import ru.practicum.event.dto.EventWithCountConfirmedRequests;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.model.Request;
+import ru.practicum.request.model.RequestStatus;
+
+import java.util.List;
+
+public interface RequestRepository extends JpaRepository {
+
+ @Query("""
+ SELECT NEW ru.practicum.event.dto.EventWithCountConfirmedRequests(r.event.id, COUNT(r))
+ FROM Request r
+ WHERE r.event.id IN :eventIds AND r.status = ru.practicum.request.model.RequestStatus.CONFIRMED
+ GROUP BY r.event.id"""
+ )
+ List findCountConfirmedRequestsByEventIds(@Param("eventIds") List eventIds);
+
+ @Query("""
+ SELECT r FROM Request r
+ LEFT JOIN FETCH r.event
+ LEFT JOIN FETCH r.requester
+ WHERE r.event.id = :eventId"""
+ )
+ List findAllParticipationRequestByEventId(@Param("eventId") Long eventId);
+
+ @Query("""
+ SELECT r FROM Request r
+ LEFT JOIN FETCH r.event
+ LEFT JOIN FETCH r.requester
+ WHERE r.id IN :requestIds"""
+ )
+ List findAllRequestById(@Param("requestIds") List requestIds);
+
+ @Query("""
+ SELECT NEW ru.practicum.request.dto.ParticipationRequestDto(
+ r.id,
+ r.created,
+ r.event.id,
+ r.requester.id,
+ r.status)
+ FROM Request r
+ WHERE r.requester.id = :userId"""
+ )
+ List findAllRequestsByUserId(@Param("userId") Long userId);
+
+ boolean existsByEventIdAndRequesterId(Long eventId, Long userId);
+
+ int countByEventIdAndStatus(Long id, RequestStatus requestStatus);
+
+ List findAllByEventIdAndStatus(Long eventId, RequestStatus requestStatus);
+}
diff --git a/main-service/src/main/java/ru/practicum/request/service/RequestService.java b/main-service/src/main/java/ru/practicum/request/service/RequestService.java
new file mode 100644
index 0000000..b130be2
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/request/service/RequestService.java
@@ -0,0 +1,19 @@
+package ru.practicum.request.service;
+
+import ru.practicum.request.dto.EventRequestStatusUpdateRequest;
+import ru.practicum.request.dto.EventRequestStatusUpdateResult;
+import ru.practicum.request.dto.ParticipationRequestDto;
+
+import java.util.List;
+
+public interface RequestService {
+ List getRequestsByEvent(Long userId, Long eventId);
+
+ EventRequestStatusUpdateResult updateRequestStatus(Long userId, Long eventId, EventRequestStatusUpdateRequest request);
+
+ List getRequestsByUserId(Long userId);
+
+ ParticipationRequestDto addRequest(Long userId, Long eventId);
+
+ ParticipationRequestDto cancelRequest(Long userId, Long requestId);
+}
diff --git a/main-service/src/main/java/ru/practicum/request/service/RequestServiceImpl.java b/main-service/src/main/java/ru/practicum/request/service/RequestServiceImpl.java
new file mode 100644
index 0000000..fefffe4
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/request/service/RequestServiceImpl.java
@@ -0,0 +1,212 @@
+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.model.EventState;
+import ru.practicum.event.service.EventService;
+import ru.practicum.exception.ConflictException;
+import ru.practicum.exception.NotFoundException;
+import ru.practicum.exception.ValidationException;
+import ru.practicum.request.dto.EventRequestStatusUpdateRequest;
+import ru.practicum.request.dto.EventRequestStatusUpdateResult;
+import ru.practicum.request.dto.ParticipationRequestDto;
+import ru.practicum.request.mapper.RequestMapper;
+import ru.practicum.request.model.Request;
+import ru.practicum.request.model.RequestStatus;
+import ru.practicum.request.repository.RequestRepository;
+import ru.practicum.user.model.User;
+import ru.practicum.user.service.UserService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class RequestServiceImpl implements RequestService {
+ private final EventService eventService;
+ private final RequestRepository requestRepository;
+ private final RequestMapper requestMapper;
+ private final UserService userService;
+
+ @Override
+ @Transactional
+ public ParticipationRequestDto addRequest(Long userId, Long eventId) {
+ User user = userService.getUserByIdOrThrow(userId);
+ Event event = eventService.getEventOrThrow(eventId);
+
+ if (requestRepository.existsByEventIdAndRequesterId(eventId, userId))
+ throw new ConflictException("Нельзя добавить повторный запрос");
+
+ if (event.getInitiator().getId().equals(userId))
+ throw new ConflictException("Инициатор события не может добавить запрос на участие в своём событии");
+
+ if (event.getState() != EventState.PUBLISHED)
+ throw new ConflictException("Нельзя участвовать в неопубликованном событии");
+
+ if (!hasSlots(event)) {
+ throw new ConflictException("Достигнут лимит по количеству участников события с id=" + eventId);
+ }
+
+ Request request = requestMapper.toEntity(event, user, RequestStatus.PENDING);
+
+ if (!event.isRequestModeration() || event.getParticipantLimit() == 0) {
+ log.debug("Модерация заявок на участие в событии с id={} не требуется", eventId);
+ request.confirmed();
+ }
+
+ request = requestRepository.save(request);
+
+ log.info("Добавление нового запроса на участие в событии с id={} от пользователя с id={}", eventId, userId);
+ return requestMapper.toDto(request);
+ }
+
+ @Override
+ public List getRequestsByUserId(Long userId) {
+ userService.getUserByIdOrThrow(userId);
+
+ log.info("Получение информации о заявках на участие пользователя с id={}", userId);
+ return requestRepository.findAllRequestsByUserId(userId);
+ }
+
+ @Override
+ public ParticipationRequestDto cancelRequest(Long userId, Long requestId) {
+ userService.getUserByIdOrThrow(userId);
+ Request request = getRequestOrThrow(requestId);
+
+ if (!request.getRequester().getId().equals(userId)) {
+ throw new ConflictException("Запрос с id=" + requestId + " не принадлежит пользователю с id=" + userId);
+ }
+
+ request.canceled();
+ request = requestRepository.save(request);
+ log.info("Отмена запроса на участие с id={} пользователя с id={}", requestId, userId);
+ return requestMapper.toDto(request);
+ }
+
+ @Override
+ public List getRequestsByEvent(Long userId, Long eventId) {
+ Event event = eventService.getEventOrThrow(eventId);
+
+ if (!event.getInitiator().getId().equals(userId))
+ throw new ValidationException("Пользователь с id=" + userId + " не является создателем события");
+
+ log.info("Получение информации о запросах на участие в событии с id={}", eventId);
+ return requestRepository.findAllParticipationRequestByEventId(eventId).stream()
+ .map(requestMapper::toDto)
+ .toList();
+ }
+
+ @Override
+ @Transactional
+ public EventRequestStatusUpdateResult updateRequestStatus(Long userId, Long eventId, EventRequestStatusUpdateRequest request) {
+ Event event = eventService.getEventOrThrow(eventId);
+
+ if (!event.getInitiator().getId().equals(userId))
+ throw new ValidationException("Пользователь с id=" + userId + " не является создателем события");
+
+ if (!event.isRequestModeration() || event.getParticipantLimit() == 0)
+ throw new ValidationException("Для данного события подтверждение заявок не требуется");
+
+ RequestStatus newStatus = request.status();
+ if (newStatus == RequestStatus.PENDING)
+ throw new ValidationException("Устанавливать можно только статусы CONFIRMED или REJECTED");
+
+ List requestsForUpdate = requestRepository.findAllRequestById(request.requestIds());
+
+ validateAllRequestsExist(request.requestIds(), requestsForUpdate);
+ validateRequestsState(requestsForUpdate, eventId);
+
+ int currentConfirmedCount = requestRepository.countByEventIdAndStatus(eventId, RequestStatus.CONFIRMED);
+ int availableSlots = event.getParticipantLimit() - currentConfirmedCount;
+
+ List confirmedRequests = new ArrayList<>();
+ List rejectedRequests = new ArrayList<>();
+
+ if (newStatus == RequestStatus.CONFIRMED) {
+ if (availableSlots <= 0) throw new ConflictException("Свободных мест больше нет");
+
+ int confirmedCount = 0;
+ for (Request req : requestsForUpdate) {
+ if (confirmedCount < availableSlots) {
+ req.confirmed();
+ confirmedRequests.add(requestMapper.toDto(req));
+ confirmedCount++;
+ } else {
+ req.rejected();
+ rejectedRequests.add(requestMapper.toDto(req));
+ }
+ }
+
+ if (currentConfirmedCount + confirmedCount >= event.getParticipantLimit()) {
+ List pendingRequests = requestRepository
+ .findAllByEventIdAndStatus(eventId, RequestStatus.PENDING);
+
+ for (Request pendingReq : pendingRequests) {
+ pendingReq.rejected();
+ rejectedRequests.add(requestMapper.toDto(pendingReq));
+ }
+
+ if (!pendingRequests.isEmpty()) {
+ requestRepository.saveAll(pendingRequests);
+ log.info("Автоматически отклонено {} заявок из-за исчерпания лимита на событие с id={}",
+ pendingRequests.size(), eventId);
+ }
+ }
+ } else {
+ for (Request req : requestsForUpdate) {
+ req.rejected();
+ rejectedRequests.add(requestMapper.toDto(req));
+ }
+ }
+
+ requestRepository.saveAll(requestsForUpdate);
+
+ log.info("Обновление статусов заявок на участие в событии с id={}: подтверждено={}, отклонено={}",
+ eventId, confirmedRequests.size(), rejectedRequests.size());
+
+ return requestMapper.toEventRequestStatusUpdateResultDto(confirmedRequests, rejectedRequests);
+ }
+
+ private Request getRequestOrThrow(Long requestId) {
+ return requestRepository.findById(requestId)
+ .orElseThrow(() -> new NotFoundException("Запрос с id=" + requestId + " не найден"));
+ }
+
+ private boolean hasSlots(Event event) {
+ Integer limit = event.getParticipantLimit();
+ if (limit == null || limit == 0) return true;
+ long confirmed = requestRepository.countByEventIdAndStatus(event.getId(), RequestStatus.CONFIRMED);
+ return confirmed < limit;
+ }
+
+ private void validateAllRequestsExist(List requestedIds, List foundRequests) {
+ List foundIds = foundRequests.stream()
+ .map(Request::getId)
+ .toList();
+
+ List missingIds = requestedIds.stream()
+ .filter(id -> !foundIds.contains(id))
+ .toList();
+
+ if (!missingIds.isEmpty()) {
+ throw new NotFoundException("Запрос(ы) с id=" + missingIds + " не найден(ы)");
+ }
+ }
+
+ private void validateRequestsState(List requests, Long eventId) {
+ for (Request req : requests) {
+ if (req.getStatus() != RequestStatus.PENDING) {
+ throw new ConflictException("Можно изменять только запросы в статусе PENDING");
+ }
+
+ if (!req.getEvent().getId().equals(eventId)) {
+ throw new ConflictException("Запрос с id=" + req.getId() + " не относится к событию с id=" + eventId);
+ }
+ }
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/user/controller/AdminUserController.java b/main-service/src/main/java/ru/practicum/user/controller/AdminUserController.java
new file mode 100644
index 0000000..b55727b
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/user/controller/AdminUserController.java
@@ -0,0 +1,48 @@
+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.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+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
+@RestController
+@RequestMapping("/admin/users")
+@RequiredArgsConstructor
+public class AdminUserController {
+ private final UserService userService;
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public UserDto createUser(@RequestBody @Valid NewUserRequest userRequest) {
+ log.debug("Controller: createUser {}", userRequest);
+ return userService.createUser(userRequest);
+ }
+
+ @GetMapping
+ public List getUsers(@RequestParam(required = false) List ids,
+ @RequestParam(defaultValue = "0") @PositiveOrZero int from,
+ @RequestParam(defaultValue = "10") @Positive int size
+ ) {
+ Pageable pageable = PageRequest.of(from / size, size);
+ log.debug("Controller: getUsers ids={}", ids);
+ return userService.getUsers(ids, pageable);
+ }
+
+ @DeleteMapping("/{userId}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void deleteUser(@PathVariable @Positive Long userId) {
+ log.debug("Controller: deleteUser id={}", userId);
+ userService.deleteUser(userId);
+ }
+}
diff --git a/main-service/src/main/java/ru/practicum/user/dto/NewUserRequest.java b/main-service/src/main/java/ru/practicum/user/dto/NewUserRequest.java
new file mode 100644
index 0000000..48aebaf
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/user/dto/NewUserRequest.java
@@ -0,0 +1,17 @@
+package ru.practicum.user.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+public record NewUserRequest(
+ @NotBlank(message = "Имя пользователя не может быть пустым")
+ @Size(min = 2, max = 250)
+ String name,
+
+ @NotBlank(message = "Email пользователя не может быть пустой")
+ @Email(message = "Email пользователя введен не верно")
+ @Size(min = 6, max = 254)
+ String email
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/user/dto/UserDto.java b/main-service/src/main/java/ru/practicum/user/dto/UserDto.java
new file mode 100644
index 0000000..9a73a41
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/user/dto/UserDto.java
@@ -0,0 +1,8 @@
+package ru.practicum.user.dto;
+
+public record UserDto(
+ Long id,
+ String name,
+ String email
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/user/dto/UserShortDto.java b/main-service/src/main/java/ru/practicum/user/dto/UserShortDto.java
new file mode 100644
index 0000000..e540cf6
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/user/dto/UserShortDto.java
@@ -0,0 +1,7 @@
+package ru.practicum.user.dto;
+
+public record UserShortDto(
+ Long id,
+ String name
+) {
+}
diff --git a/main-service/src/main/java/ru/practicum/user/mapper/UserMapper.java b/main-service/src/main/java/ru/practicum/user/mapper/UserMapper.java
new file mode 100644
index 0000000..a0eefc6
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/user/mapper/UserMapper.java
@@ -0,0 +1,19 @@
+package ru.practicum.user.mapper;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import ru.practicum.user.dto.NewUserRequest;
+import ru.practicum.user.dto.UserDto;
+import ru.practicum.user.dto.UserShortDto;
+import ru.practicum.user.model.User;
+
+@Mapper(componentModel = "spring")
+public interface UserMapper {
+
+ @Mapping(target = "id", ignore = true)
+ User toEntity(NewUserRequest dto);
+
+ UserDto toDto(User user);
+
+ UserShortDto toShortDto(User user);
+}
diff --git a/main-service/src/main/java/ru/practicum/user/model/User.java b/main-service/src/main/java/ru/practicum/user/model/User.java
new file mode 100644
index 0000000..036864c
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/user/model/User.java
@@ -0,0 +1,26 @@
+package ru.practicum.user.model;
+
+import jakarta.persistence.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+@Entity
+@Table(name = "users")
+@Getter
+@Setter
+@ToString
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class User {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
+ private Long id;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false, unique = true)
+ private String email;
+}
diff --git a/main-service/src/main/java/ru/practicum/user/repository/UserRepository.java b/main-service/src/main/java/ru/practicum/user/repository/UserRepository.java
new file mode 100644
index 0000000..e3d8095
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/user/repository/UserRepository.java
@@ -0,0 +1,14 @@
+package ru.practicum.user.repository;
+
+import org.springframework.data.domain.Page;
+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);
+
+ Page findByIdIn(List ids, Pageable pageable);
+}
diff --git a/main-service/src/main/java/ru/practicum/user/service/UserService.java b/main-service/src/main/java/ru/practicum/user/service/UserService.java
new file mode 100644
index 0000000..b694803
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/user/service/UserService.java
@@ -0,0 +1,20 @@
+package ru.practicum.user.service;
+
+import org.springframework.data.domain.Pageable;
+import ru.practicum.user.dto.NewUserRequest;
+import ru.practicum.user.dto.UserDto;
+import ru.practicum.user.model.User;
+
+import java.util.List;
+
+public interface UserService {
+ UserDto createUser(NewUserRequest userRequest);
+
+ List getUsers(List ids, Pageable pageable);
+
+ void deleteUser(Long userId);
+
+ User getUserById(Long userId);
+
+ User getUserByIdOrThrow(Long userId);
+}
diff --git a/main-service/src/main/java/ru/practicum/user/service/UserServiceImpl.java b/main-service/src/main/java/ru/practicum/user/service/UserServiceImpl.java
new file mode 100644
index 0000000..f9666c0
--- /dev/null
+++ b/main-service/src/main/java/ru/practicum/user/service/UserServiceImpl.java
@@ -0,0 +1,77 @@
+package ru.practicum.user.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.data.domain.Sort;
+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.mapper.UserMapper;
+import ru.practicum.user.model.User;
+import ru.practicum.user.repository.UserRepository;
+
+import java.util.List;
+
+import static org.springframework.data.domain.Sort.Direction.ASC;
+
+@Slf4j
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class UserServiceImpl implements UserService {
+ private final UserRepository userRepository;
+ private final UserMapper userMapper;
+
+ @Override
+ @Transactional
+ public UserDto createUser(NewUserRequest userRequest) {
+ if (userRepository.existsByEmail(userRequest.email())) {
+ throw new ConflictException("Пользователь с таким e-mail уже существует: " + userRequest.email());
+ }
+
+ User user = userMapper.toEntity(userRequest);
+ user = userRepository.save(user);
+ log.info("Добавлен новый пользователь {}", user);
+ return userMapper.toDto(user);
+ }
+
+ @Override
+ public List getUsers(List ids, Pageable pageable) {
+ Pageable sortedPageable = PageRequest.of(
+ pageable.getPageNumber(),
+ pageable.getPageSize(),
+ Sort.by(ASC, "id")
+ );
+ Page users = (ids == null || ids.isEmpty())
+ ? userRepository.findAll(sortedPageable)
+ : userRepository.findByIdIn(ids, sortedPageable);
+ return users.stream()
+ .map(userMapper::toDto)
+ .toList();
+ }
+
+ @Override
+ @Transactional
+ public void deleteUser(Long userId) {
+ getUserByIdOrThrow(userId);
+ log.info("Удален пользователь с id {}", userId);
+ userRepository.deleteById(userId);
+ }
+
+ @Override
+ public User getUserById(Long userId) {
+ return getUserByIdOrThrow(userId);
+ }
+
+ @Override
+ public User getUserByIdOrThrow(Long userId) {
+ return userRepository.findById(userId)
+ .orElseThrow(() -> new NotFoundException("Пользователь с id=" + userId + " не найден"));
+ }
+}
diff --git a/main-service/src/main/resources/application-local.properties b/main-service/src/main/resources/application-local.properties
new file mode 100644
index 0000000..4500b27
--- /dev/null
+++ b/main-service/src/main/resources/application-local.properties
@@ -0,0 +1,19 @@
+# Server Configuration
+server.port=8080
+stats.server.url=http://localhost:9090
+# JPA / Hibernate
+spring.jpa.hibernate.ddl-auto=create
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.show-sql=false
+spring.sql.init.mode=always
+# DataSource
+spring.datasource.driverClassName=org.postgresql.Driver
+spring.datasource.url=jdbc:postgresql://localhost:6543/mainDB_local
+spring.datasource.username=localuser
+spring.datasource.password=localpass
+# Logging
+logging.level.ru.practicum=DEBUG
+logging.level.org.springframework.orm.jpa=INFO
+logging.level.org.springframework.transaction=INFO
+logging.level.org.springframework.transaction.interceptor=DEBUG
+logging.level.org.springframework.orm.jpa.JpaTransactionManager=INFO
\ No newline at end of file
diff --git a/main-service/src/main/resources/application-test.properties b/main-service/src/main/resources/application-test.properties
new file mode 100644
index 0000000..0171bc3
--- /dev/null
+++ b/main-service/src/main/resources/application-test.properties
@@ -0,0 +1,15 @@
+# JPA / Hibernate
+spring.jpa.hibernate.ddl-auto=none
+spring.jpa.properties.hibernate.format_sql=true
+spring.sql.init.mode=always
+# DataSource
+spring.datasource.url=jdbc:h2:mem:main;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=main
+spring.datasource.password=main
+# Logging
+logging.level.ru.practicum=DEBUG
+logging.level.org.springframework.orm.jpa=INFO
+logging.level.org.springframework.transaction=DEBUG
+logging.level.org.springframework.transaction.interceptor=DEBUG
+logging.level.org.springframework.orm.jpa.JpaTransactionManager=INFO
\ No newline at end of file
diff --git a/main-service/src/main/resources/application.properties b/main-service/src/main/resources/application.properties
index 6885798..d531835 100644
--- a/main-service/src/main/resources/application.properties
+++ b/main-service/src/main/resources/application.properties
@@ -1,11 +1,18 @@
+# Server Configuration
server.port=8080
-stats.server.url=http://localhost:9090
-
-spring.jpa.show-sql=true
-spring.jpa.hibernate.ddl-auto=update
+stats.server.url=${STATS_SERVER_URL}
+# JPA / Hibernate
+spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.format_sql=true
-
+spring.jpa.show-sql=false
+spring.sql.init.mode=always
+# DataSource
spring.datasource.driverClassName=org.postgresql.Driver
-spring.datasource.url=jdbc:postgresql://localhost:6542/mainDB
-spring.datasource.username=dbuser
-spring.datasource.password=12345
\ No newline at end of file
+spring.datasource.url=${SPRING_DATASOURCE_URL}
+spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
+spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
+# Logging
+logging.level.org.springframework.orm.jpa=INFO
+logging.level.org.springframework.transaction=INFO
+logging.level.org.springframework.transaction.interceptor=DEBUG
+logging.level.org.springframework.orm.jpa.JpaTransactionManager=INFO
\ No newline at end of file
diff --git a/main-service/src/main/resources/schema.sql b/main-service/src/main/resources/schema.sql
new file mode 100644
index 0000000..fab07f9
--- /dev/null
+++ b/main-service/src/main/resources/schema.sql
@@ -0,0 +1,48 @@
+create TABLE IF NOT EXISTS categories (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ name VARCHAR(50) NOT NULL UNIQUE
+);
+
+create TABLE IF NOT EXISTS users (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL UNIQUE
+);
+
+create TABLE IF NOT EXISTS events (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ title VARCHAR(120) NOT NULL,
+ annotation VARCHAR(2000) NOT NULL,
+ description VARCHAR(7000) NOT NULL,
+ category_id BIGINT REFERENCES categories(id),
+ initiator_id BIGINT REFERENCES users (id),
+ created_on TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ event_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ published_on TIMESTAMP WITHOUT TIME ZONE,
+ paid BOOLEAN NOT NULL,
+ participant_limit INTEGER NOT NULL,
+ request_moderation BOOLEAN NOT NULL,
+ state VARCHAR(20) NOT NULL,
+ lat FLOAT,
+ lon FLOAT
+);
+
+create TABLE IF NOT EXISTS requests (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ event_id BIGINT REFERENCES events(id),
+ requester_id BIGINT REFERENCES users(id),
+ created TIMESTAMP WITHOUT TIME ZONE,
+ status VARCHAR(255)
+);
+
+create TABLE IF NOT EXISTS compilations (
+ id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ title VARCHAR(50) NOT NULL,
+ pinned BOOLEAN NOT NULL
+);
+
+create TABLE IF NOT EXISTS compilation_events (
+ compilation_id BIGINT NOT NULL REFERENCES compilations (id),
+ event_id BIGINT NOT NULL REFERENCES events (id),
+ CONSTRAINT pk_compilation_events PRIMARY KEY (compilation_id, event_id)
+);
\ No newline at end of file
diff --git a/stats/stats-client/src/main/java/ru/practicum/StatsClient.java b/stats/stats-client/src/main/java/ru/practicum/StatsClient.java
index 6851b2a..2cbc962 100644
--- a/stats/stats-client/src/main/java/ru/practicum/StatsClient.java
+++ b/stats/stats-client/src/main/java/ru/practicum/StatsClient.java
@@ -14,11 +14,13 @@
import java.time.format.DateTimeFormatter;
import java.util.List;
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
@Slf4j
@Component
public class StatsClient {
private final RestClient restClient;
- private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+ private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
public StatsClient(@Value("${stats-server.url:http://localhost:9090}") String serverUrl) {
this.restClient = RestClient.builder()
@@ -44,7 +46,7 @@ public void saveHit(CreateEndpointHitDto createEndpointHitDto) {
}
public List getStats(LocalDateTime start, LocalDateTime end,
- List uris, Boolean unique) {
+ List uris, boolean unique) {
try {
List result = restClient.get()
.uri(uriBuilder -> {
@@ -55,9 +57,9 @@ public List getStats(LocalDateTime start, LocalDateTime end,
if (uris != null && !uris.isEmpty()) {
uris.forEach(uri -> builder.queryParam("uris", uri));
}
- if (unique != null) {
- builder.queryParam("unique", unique);
- }
+
+ builder.queryParam("unique", unique);
+
return builder.build();
})
.retrieve()
@@ -65,11 +67,11 @@ public List getStats(LocalDateTime start, LocalDateTime end,
});
log.debug("Получено записей статистики: {}", result != null ? result.size() : 0);
- return result != null ? result : List.of();
+ return result;
} catch (Exception exception) {
log.error("Ошибка при получении статистики: {}", exception.getMessage());
- throw new StatsClientException("Couldn't get statistics", exception);
+ throw new StatsClientException("Statistics could not be retrieved", exception);
}
}
}
diff --git a/stats/stats-client/src/main/resources/application.properties b/stats/stats-client/src/main/resources/application.properties
deleted file mode 100644
index 6d6b68c..0000000
--- a/stats/stats-client/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-stats.server.url=http://localhost:9090
\ No newline at end of file
diff --git a/stats/stats-dto/src/main/java/ru/practicum/CreateEndpointHitDto.java b/stats/stats-dto/src/main/java/ru/practicum/CreateEndpointHitDto.java
index d3ae6eb..2ff39fa 100644
--- a/stats/stats-dto/src/main/java/ru/practicum/CreateEndpointHitDto.java
+++ b/stats/stats-dto/src/main/java/ru/practicum/CreateEndpointHitDto.java
@@ -6,6 +6,8 @@
import java.time.LocalDateTime;
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
public record CreateEndpointHitDto(
@NotBlank(message = "Название приложения не может быть пустым")
String app,
@@ -17,7 +19,7 @@ public record CreateEndpointHitDto(
String ip,
@NotNull(message = "Временная метка не может быть null")
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_PATTERN)
LocalDateTime timestamp
) {
}
diff --git a/stats/stats-dto/src/main/java/ru/practicum/StatsRequest.java b/stats/stats-dto/src/main/java/ru/practicum/StatsRequest.java
index 6db7ac9..329a487 100644
--- a/stats/stats-dto/src/main/java/ru/practicum/StatsRequest.java
+++ b/stats/stats-dto/src/main/java/ru/practicum/StatsRequest.java
@@ -4,6 +4,8 @@
import java.time.format.DateTimeFormatter;
import java.util.List;
+import static ru.practicum.constants.DateTimeConstants.DATE_TIME_PATTERN;
+
public record StatsRequest(
LocalDateTime start,
LocalDateTime end,
@@ -11,7 +13,7 @@ public record StatsRequest(
boolean unique
) {
public static StatsRequest of(String start, String end, List uris, boolean unique) {
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
return new StatsRequest(
LocalDateTime.parse(start, formatter),
LocalDateTime.parse(end, formatter),
diff --git a/stats/stats-dto/src/main/java/ru/practicum/constants/DateTimeConstants.java b/stats/stats-dto/src/main/java/ru/practicum/constants/DateTimeConstants.java
new file mode 100644
index 0000000..276e937
--- /dev/null
+++ b/stats/stats-dto/src/main/java/ru/practicum/constants/DateTimeConstants.java
@@ -0,0 +1,9 @@
+package ru.practicum.constants;
+
+public final class DateTimeConstants {
+
+ private DateTimeConstants() {
+ }
+
+ public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
+}
diff --git a/stats/stats-server/src/main/java/ru/practicum/controller/StatsController.java b/stats/stats-server/src/main/java/ru/practicum/controller/StatsController.java
index 1387c1f..e4f6d1f 100644
--- a/stats/stats-server/src/main/java/ru/practicum/controller/StatsController.java
+++ b/stats/stats-server/src/main/java/ru/practicum/controller/StatsController.java
@@ -8,6 +8,7 @@
import ru.practicum.CreateEndpointHitDto;
import ru.practicum.StatsRequest;
import ru.practicum.ViewStatsDto;
+import ru.practicum.exception.BadRequestException;
import ru.practicum.service.StatsService;
import java.util.List;
@@ -20,18 +21,22 @@ public class StatsController {
@PostMapping("/hit")
@ResponseStatus(HttpStatus.CREATED)
- public void createHit(@Valid @RequestBody CreateEndpointHitDto createEndpointHitDto) {
- log.info("Controller: createHit requestBody={}", createEndpointHitDto);
+ public void createHit(@RequestBody @Valid CreateEndpointHitDto createEndpointHitDto) {
+ log.debug("Controller: createHit requestBody={}", createEndpointHitDto);
statsService.createHit(createEndpointHitDto);
}
@GetMapping("/stats")
public List getStats(@RequestParam("start") String start, @RequestParam("end") String end,
- @RequestParam(value = "uris", required = false) List uris,
- @RequestParam(value = "unique", defaultValue = "false") boolean unique) {
- log.debug("start={}, end={}, uris={}, unique={}", start, end, uris, unique);
+ @RequestParam(value = "uris", required = false) List uris,
+ @RequestParam(value = "unique", defaultValue = "false") boolean unique) {
StatsRequest request = StatsRequest.of(start, end, uris, unique);
- log.info("Controller: getStats request={}", request);
+
+ if (request.start().isAfter(request.end())) {
+ throw new BadRequestException("Дата начала должна быть раньше даты окончания");
+ }
+
+ log.debug("Controller: getStats request={}", request);
return statsService.getStats(request);
}
}
diff --git a/stats/stats-server/src/main/java/ru/practicum/exception/BadRequestException.java b/stats/stats-server/src/main/java/ru/practicum/exception/BadRequestException.java
new file mode 100644
index 0000000..7aee4bc
--- /dev/null
+++ b/stats/stats-server/src/main/java/ru/practicum/exception/BadRequestException.java
@@ -0,0 +1,7 @@
+package ru.practicum.exception;
+
+public class BadRequestException extends RuntimeException {
+ public BadRequestException(String message) {
+ super(message);
+ }
+}
diff --git a/stats/stats-server/src/main/java/ru/practicum/exception/ErrorHandler.java b/stats/stats-server/src/main/java/ru/practicum/exception/ErrorHandler.java
new file mode 100644
index 0000000..8fb199b
--- /dev/null
+++ b/stats/stats-server/src/main/java/ru/practicum/exception/ErrorHandler.java
@@ -0,0 +1,29 @@
+package ru.practicum.exception;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class ErrorHandler {
+ @ExceptionHandler(BadRequestException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ErrorResponse handleBadRequestException(BadRequestException exception, HttpServletRequest request) {
+ return buildResponse(HttpStatus.BAD_REQUEST, exception, request);
+ }
+
+ private ErrorResponse buildResponse(HttpStatus httpStatus, Exception exception, HttpServletRequest request) {
+ return buildResponse(httpStatus, exception.getMessage(), exception, request);
+ }
+
+ private ErrorResponse buildResponse(HttpStatus httpStatus, String message, Exception exception, HttpServletRequest request) {
+ String path = request.getRequestURI();
+ String httpMethod = request.getMethod();
+ int statusCode = httpStatus.value();
+ String error = httpStatus.getReasonPhrase();
+
+ return new ErrorResponse(path, httpMethod, statusCode, error, message);
+ }
+}
diff --git a/stats/stats-server/src/main/java/ru/practicum/exception/ErrorResponse.java b/stats/stats-server/src/main/java/ru/practicum/exception/ErrorResponse.java
new file mode 100644
index 0000000..31994d6
--- /dev/null
+++ b/stats/stats-server/src/main/java/ru/practicum/exception/ErrorResponse.java
@@ -0,0 +1,10 @@
+package ru.practicum.exception;
+
+public record ErrorResponse(
+ String path,
+ String httpMethod,
+ int statusCode,
+ String error,
+ String message
+) {
+}
\ No newline at end of file
diff --git a/stats/stats-server/src/main/java/ru/practicum/model/EndpointHit.java b/stats/stats-server/src/main/java/ru/practicum/model/EndpointHit.java
index be82130..5c73895 100644
--- a/stats/stats-server/src/main/java/ru/practicum/model/EndpointHit.java
+++ b/stats/stats-server/src/main/java/ru/practicum/model/EndpointHit.java
@@ -1,19 +1,21 @@
package ru.practicum.model;
import jakarta.persistence.*;
-import lombok.*;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Table(name = "endpoint_hits")
@Getter
-@Setter(AccessLevel.PROTECTED)
-@ToString
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Setter
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class EndpointHit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
+ @EqualsAndHashCode.Include
@Column(name = "hit_id")
private Long id;
@@ -21,23 +23,4 @@ public class EndpointHit {
private String uri;
private String ip;
private LocalDateTime timestamp;
-
- public EndpointHit(String app, String uri, String ip, LocalDateTime timestamp) {
- this.app = app;
- this.uri = uri;
- this.ip = ip;
- this.timestamp = timestamp;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof EndpointHit)) return false;
- return id != null && id.equals(((EndpointHit) o).getId());
- }
-
- @Override
- public int hashCode() {
- return id == null ? 0 : id.hashCode();
- }
}
diff --git a/stats/stats-server/src/main/resources/application-local.properties b/stats/stats-server/src/main/resources/application-local.properties
new file mode 100644
index 0000000..fffbce2
--- /dev/null
+++ b/stats/stats-server/src/main/resources/application-local.properties
@@ -0,0 +1,18 @@
+# Server Configuration
+server.port=9090
+# JPA / Hibernate
+spring.jpa.hibernate.ddl-auto=create
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.show-sql=false
+spring.sql.init.mode=always
+# DataSource
+spring.datasource.driverClassName=org.postgresql.Driver
+spring.datasource.url=jdbc:postgresql://localhost:6542/statDB_local
+spring.datasource.username=localuser
+spring.datasource.password=localpass
+# Logging
+logging.level.ru.practicum=DEBUG
+logging.level.org.springframework.orm.jpa=INFO
+logging.level.org.springframework.transaction=INFO
+logging.level.org.springframework.transaction.interceptor=DEBUG
+logging.level.org.springframework.orm.jpa.JpaTransactionManager=INFO
\ No newline at end of file
diff --git a/stats/stats-server/src/main/resources/application-test.properties b/stats/stats-server/src/main/resources/application-test.properties
index 7345fa2..768c576 100644
--- a/stats/stats-server/src/main/resources/application-test.properties
+++ b/stats/stats-server/src/main/resources/application-test.properties
@@ -1,12 +1,13 @@
+# JPA / Hibernate
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.format_sql=true
spring.sql.init.mode=always
-
+# DataSource
+spring.datasource.url=jdbc:h2:mem:stat;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH
spring.datasource.driverClassName=org.h2.Driver
-spring.datasource.url=jdbc:h2:mem:stat
spring.datasource.username=stat
spring.datasource.password=stat
-
+# Logging
logging.level.ru.practicum=DEBUG
logging.level.org.springframework.orm.jpa=INFO
logging.level.org.springframework.transaction=DEBUG
diff --git a/stats/stats-server/src/main/resources/application.properties b/stats/stats-server/src/main/resources/application.properties
index 85cfdaf..d18474c 100644
--- a/stats/stats-server/src/main/resources/application.properties
+++ b/stats/stats-server/src/main/resources/application.properties
@@ -1,14 +1,16 @@
+# Server Configuration
server.port=9090
+# JPA / Hibernate
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=false
spring.sql.init.mode=always
-
+# DataSource
spring.datasource.driverClassName=org.postgresql.Driver
-spring.datasource.url=jdbc:postgresql://localhost:5432/statDB
-spring.datasource.username=dbuser
-spring.datasource.password=12345
-
+spring.datasource.url=${SPRING_DATASOURCE_URL}
+spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
+spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
+# Logging
logging.level.org.springframework.orm.jpa=INFO
logging.level.org.springframework.transaction=INFO
logging.level.org.springframework.transaction.interceptor=DEBUG