diff --git a/borschevskaya_anna_lab_3/README.md b/borschevskaya_anna_lab_3/README.md
new file mode 100644
index 0000000..0aabe86
--- /dev/null
+++ b/borschevskaya_anna_lab_3/README.md
@@ -0,0 +1,58 @@
+# Отчет. Лабораторная работа 3
+
+## Описание
+В рамках лабораторной работы № 3 были реализованы два сервиса (Java + Spring), осуществляющие CRUD-операции над сущностями.
+
+Модель данных следующая:
+
+Сущность "Компания" (сервис company)
+- идентификатор компании
+- название
+- адрес
+
+Сущность "Вакансия" (сервис vacancy)
+- идентификатор вакансии
+- название
+- описание
+- нижняя граница зарплаты
+- верхняя граница зарплаты
+- идентификатор компании
+
+Компания с вакансией связана как "один ко многим".
+
+Каждый из сервисов имеет API с пятью эндпоинтами. При этом в сервисе vacancy при запросе вакансии по id происходит
+дополнительный запрос в сервис company для получения информации по компании, связанной с вакансией.
+Происходит это взаимодействие с помощью библиотеки OpenFeign, которая "под капотом" использует HttpClient.
+
+В качестве хранилища данных использовалась СУБД Postgres. У каждого сервиса своя база данных в поднятой СУБД.
+Для создания схемы БД была использована библиотека Flyway, которая применила написанные миграции при старте приложения.
+
+Запросы к сервисам проксирует шлюз на основе Nginx. Для этого перед запуском nginx был описан конфигурационный файл nginx.conf,
+в котором описан прослушиваемый порт и название сервера (в блоке server), маршруты до микросервисов и параметры проксирования (в блоке location).
+
+Таким образом, с помощью Docker Compose были подняты сервисы:
+- company
+- vacancy
+- postgres
+- nginx
+## Как запустить
+Для того, чтобы запустить сервисы, необходимо выполнить следующие действия:
+1. Установить и запустить Docker Engine или Docker Desktop
+2. Через консоль перейти в папку, в которой расположен файл docker-compose.yml
+3. Выполнить команду:
+```
+docker compose up --build
+```
+В случае успешного запуска всех контейнеров в консоли будет выведено следующее сообщение:
+```
+[+] Running 5/5
+ ✔ Network borschevskaya_anna_lab_3_default Created 0.0s
+ ✔ Container postgres Started 0.6s
+ ✔ Container vacancy Started 1.1s
+ ✔ Container company Started 0.9s
+ ✔ Container borschevskaya_anna_lab_3-nginx-1 Started
+```
+Далее можно осуществлять запросы к сервисам по адресу http://localhost/{location}, где часть пути location меняется в зависимости от сервиса и запроса.
+## Видео-отчет
+Работоспособность лабораторной работы можно оценить в следующем [видео](https://disk.yandex.ru/i/KPNBfnlcgl1auw).
+Демонстрация взаимодействия с системой производится с применением сервиса Postman.
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/company-service/.gitignore b/borschevskaya_anna_lab_3/company-service/.gitignore
new file mode 100644
index 0000000..5ff6309
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/.gitignore
@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/company-service/Dockerfile b/borschevskaya_anna_lab_3/company-service/Dockerfile
new file mode 100644
index 0000000..cc673eb
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/Dockerfile
@@ -0,0 +1,21 @@
+# Используем образ Maven для сборки
+FROM maven:3.8-eclipse-temurin-21-alpine AS build
+
+# Устанавливаем рабочую директорию
+WORKDIR /app
+
+# Копируем остальные исходные файлы
+COPY pom.xml .
+COPY src src
+
+# Собираем весь проект
+RUN mvn clean package -DskipTests
+
+# Используем официальный образ JDK для запуска собранного jar-файла
+FROM eclipse-temurin:21-jdk-alpine
+
+# Копируем jar-файл из предыдущего этапа
+COPY --from=build /app/target/*.jar /app.jar
+
+# Указываем команду для запуска приложения
+CMD ["java", "-jar", "app.jar"]
diff --git a/borschevskaya_anna_lab_3/company-service/pom.xml b/borschevskaya_anna_lab_3/company-service/pom.xml
new file mode 100644
index 0000000..96bd4a8
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+
+ ru.somecompany
+ company-service
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.3
+
+
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.flywaydb
+ flyway-core
+
+
+ org.postgresql
+ postgresql
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+ io.swagger.core.v3
+ swagger-annotations
+ 2.2.1
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.3.0
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/CompanyApplication.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/CompanyApplication.java
new file mode 100644
index 0000000..1d4b094
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/CompanyApplication.java
@@ -0,0 +1,12 @@
+package ru.somecompany;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class CompanyApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(CompanyApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyController.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyController.java
new file mode 100644
index 0000000..addbe5d
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyController.java
@@ -0,0 +1,44 @@
+package ru.somecompany.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.constraints.NotNull;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import ru.somecompany.domain.CompanyEntity;
+import ru.somecompany.domain.CreateCompanyRequest;
+
+import java.util.List;
+import java.util.UUID;
+
+
+@Validated
+@Tag(name = "company", description = "API для управления компаниями")
+public interface CompanyController {
+
+ @Operation(summary = "Получение всех компаний")
+ @GetMapping(value = "/api/v1/company")
+ List getCompanies();
+
+ @Operation(summary = "Создание компании")
+ @PostMapping(value = "/api/v1/company")
+ CompanyEntity createCompany(@RequestBody @NotNull CreateCompanyRequest companyRequest);
+
+ @Operation(summary = "Получение информации о компании по id")
+ @GetMapping(value = "/api/v1/company/{companyId}")
+ CompanyEntity getCompany(@PathVariable UUID companyId);
+
+ @Operation(summary = "Редактирование компании")
+ @PutMapping(value = "/api/v1/company/{companyId}")
+ CompanyEntity updateCompany(@PathVariable UUID companyId,
+ @RequestBody @NotNull CreateCompanyRequest companyRequest);
+
+ @Operation(summary = "Удалении компании по id")
+ @DeleteMapping(value = "/api/v1/company/{companyId}")
+ void deleteCompany(@PathVariable UUID companyId);
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyControllerImpl.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyControllerImpl.java
new file mode 100644
index 0000000..a15ca21
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/controller/CompanyControllerImpl.java
@@ -0,0 +1,42 @@
+package ru.somecompany.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.RestController;
+import ru.somecompany.domain.CompanyEntity;
+import ru.somecompany.domain.CreateCompanyRequest;
+import ru.somecompany.service.CompanyService;
+
+import java.util.List;
+import java.util.UUID;
+
+@RestController
+@RequiredArgsConstructor
+public class CompanyControllerImpl implements CompanyController {
+
+ private final CompanyService companyService;
+
+ @Override
+ public List getCompanies() {
+ return companyService.getCompanies();
+ }
+
+ @Override
+ public CompanyEntity createCompany(CreateCompanyRequest companyRequest) {
+ return companyService.createCompany(companyRequest);
+ }
+
+ @Override
+ public CompanyEntity getCompany(UUID companyId) {
+ return companyService.getCompany(companyId);
+ }
+
+ @Override
+ public CompanyEntity updateCompany(UUID companyId, CreateCompanyRequest companyRequest) {
+ return companyService.updateCompany(companyId, companyRequest);
+ }
+
+ @Override
+ public void deleteCompany(UUID companyId) {
+ companyService.deleteCompany(companyId);
+ }
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CompanyEntity.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CompanyEntity.java
new file mode 100644
index 0000000..5097043
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CompanyEntity.java
@@ -0,0 +1,32 @@
+package ru.somecompany.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+@Data
+@Entity
+@Builder
+@Table(name = "company")
+@AllArgsConstructor
+@NoArgsConstructor
+public class CompanyEntity {
+
+ @Id
+ @GeneratedValue
+ private UUID id;
+
+ @Column(name = "name", unique = true, nullable = false)
+ private String name;
+
+ @Column(name = "address", unique = true, nullable = false)
+ private String address;
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CreateCompanyRequest.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CreateCompanyRequest.java
new file mode 100644
index 0000000..8c0a665
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/domain/CreateCompanyRequest.java
@@ -0,0 +1,13 @@
+package ru.somecompany.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class CreateCompanyRequest {
+
+ private String name;
+
+ private String address;
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java
new file mode 100644
index 0000000..fc34e6f
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java
@@ -0,0 +1,41 @@
+package ru.somecompany.exception;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+import java.time.LocalDateTime;
+
+import static org.springframework.http.HttpStatus.NOT_FOUND;
+
+@Slf4j
+public abstract class AbstractWebExceptionHandler {
+
+ public static ResponseEntity buildResponse(ErrorCode errorCode, HttpStatus status, String msg,
+ boolean details, Throwable ex) {
+ return buildResponse(errorCode.name(), status, msg, details, ex, LocalDateTime.now());
+ }
+
+ public static ResponseEntity buildResponse(String errorCode, HttpStatus status, String msg,
+ boolean details, Throwable ex, LocalDateTime timestamp) {
+ if (details) {
+ log.error(msg, ex); // with stack-trace
+ } else {
+ var message = StringUtils.equals(msg, ex.getMessage()) ? msg : msg + ": " + ex.getMessage();
+ if (status == NOT_FOUND) {
+ log.warn(message);
+ } else {
+ log.error(message);
+ }
+ }
+
+ return ResponseEntity.status(status.value())
+ .body(ResponseError.builder()
+ .code(errorCode)
+ .message(msg)
+ .timestamp(timestamp)
+ .status(status.value())
+ .build());
+ }
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/BasicError.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/BasicError.java
new file mode 100644
index 0000000..baca00a
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/BasicError.java
@@ -0,0 +1,5 @@
+package ru.somecompany.exception;
+
+public enum BasicError implements ErrorCode {
+ NOT_FOUND
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java
new file mode 100644
index 0000000..5148716
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java
@@ -0,0 +1,26 @@
+package ru.somecompany.exception;
+
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public class CompanyAppRuntimeException extends RuntimeException {
+ private final ErrorCode errorCode;
+ private final HttpStatus status;
+ private final boolean details;
+
+ public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message) {
+ this(errorCode, status, message, null);
+ }
+
+ public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message, Throwable error) {
+ this(errorCode, status, message, true, error);
+ }
+
+ public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message, boolean details, Throwable error) {
+ super(message, error);
+ this.errorCode = errorCode;
+ this.status = status;
+ this.details = details;
+ }
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ErrorCode.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ErrorCode.java
new file mode 100644
index 0000000..ed73439
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ErrorCode.java
@@ -0,0 +1,9 @@
+package ru.somecompany.exception;
+
+import java.io.Serializable;
+
+public interface ErrorCode extends Serializable {
+
+ String name();
+
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java
new file mode 100644
index 0000000..7ecd7c0
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java
@@ -0,0 +1,28 @@
+package ru.somecompany.exception;
+
+import org.springframework.http.HttpStatus;
+
+import static ru.somecompany.exception.BasicError.NOT_FOUND;
+
+public class ResourceNotFoundException extends CompanyAppRuntimeException {
+
+ public ResourceNotFoundException(ErrorCode errorCode, String message) {
+ this(errorCode, message, null);
+ }
+
+ public ResourceNotFoundException(ErrorCode errorCode, String message, Throwable error) {
+ super(errorCode, HttpStatus.NOT_FOUND, message, error);
+ }
+
+ public static ResourceNotFoundException notFound(String message, Object... args) {
+ return new ResourceNotFoundException(NOT_FOUND, String.format(message, args), null);
+ }
+
+ public static ResourceNotFoundException notFound() {
+ return new ResourceNotFoundException(NOT_FOUND, "Запрашиваемые данные не найдены", null);
+ }
+
+ public static ResourceNotFoundException notFound(Throwable error) {
+ return new ResourceNotFoundException(NOT_FOUND, error.getMessage(), error);
+ }
+}
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResponseError.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResponseError.java
new file mode 100644
index 0000000..4ae118d
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/ResponseError.java
@@ -0,0 +1,24 @@
+package ru.somecompany.exception;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ResponseError {
+ @JsonProperty(required = true)
+ private LocalDateTime timestamp;
+ @JsonProperty(required = true)
+ private Integer status;
+ @JsonProperty(required = true)
+ private String message;
+ @JsonProperty(required = true)
+ private String code;
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java
new file mode 100644
index 0000000..78fa51f
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java
@@ -0,0 +1,22 @@
+package ru.somecompany.exception;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@Getter
+@RestControllerAdvice
+@RequiredArgsConstructor
+public class WebExceptionHandler extends AbstractWebExceptionHandler {
+ private final ObjectMapper objectMapper;
+
+ @ExceptionHandler(CompanyAppRuntimeException.class)
+ public ResponseEntity handleException(CompanyAppRuntimeException ex) {
+ return buildResponse(ex.getErrorCode(), ex.getStatus(), ex.getMessage(), ex.isDetails(), ex);
+ }
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/repository/CompanyRepository.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/repository/CompanyRepository.java
new file mode 100644
index 0000000..2cce0d8
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/repository/CompanyRepository.java
@@ -0,0 +1,9 @@
+package ru.somecompany.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.somecompany.domain.CompanyEntity;
+
+import java.util.UUID;
+
+public interface CompanyRepository extends JpaRepository {
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyService.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyService.java
new file mode 100644
index 0000000..0c306e5
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyService.java
@@ -0,0 +1,20 @@
+package ru.somecompany.service;
+
+import ru.somecompany.domain.CompanyEntity;
+import ru.somecompany.domain.CreateCompanyRequest;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface CompanyService {
+
+ List getCompanies();
+
+ CompanyEntity createCompany(CreateCompanyRequest companyRequest);
+
+ CompanyEntity updateCompany(UUID companyId, CreateCompanyRequest companyRequest);
+
+ void deleteCompany(UUID companyId);
+
+ CompanyEntity getCompany(UUID companyId);
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyServiceImpl.java b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyServiceImpl.java
new file mode 100644
index 0000000..b33c2f4
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/java/ru/somecompany/service/CompanyServiceImpl.java
@@ -0,0 +1,57 @@
+package ru.somecompany.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import ru.somecompany.domain.CompanyEntity;
+import ru.somecompany.domain.CreateCompanyRequest;
+import ru.somecompany.repository.CompanyRepository;
+
+import java.util.List;
+import java.util.UUID;
+
+import static ru.somecompany.exception.ResourceNotFoundException.notFound;
+
+@Service
+@RequiredArgsConstructor
+public class CompanyServiceImpl implements CompanyService {
+
+ private final CompanyRepository companyRepository;
+
+ @Override
+ public List getCompanies() {
+ return companyRepository.findAll();
+ }
+
+ @Override
+ public CompanyEntity createCompany(CreateCompanyRequest companyRequest) {
+ var entity = CompanyEntity.builder()
+ .name(companyRequest.getName())
+ .address(companyRequest.getAddress())
+ .build();
+ return companyRepository.save(entity);
+ }
+
+ @Override
+ public CompanyEntity updateCompany(UUID companyId, CreateCompanyRequest companyRequest) {
+ var company = getById(companyId);
+ company.setName(companyRequest.getName());
+ company.setAddress(companyRequest.getAddress());
+ return companyRepository.save(company);
+ }
+
+ @Override
+ public void deleteCompany(UUID companyId) {
+ var company = getById(companyId);
+ companyRepository.delete(company);
+ }
+
+ @Override
+ public CompanyEntity getCompany(UUID companyId) {
+ return getById(companyId);
+ }
+
+ private CompanyEntity getById(UUID companyId) {
+ return companyRepository.findById(companyId)
+ .orElseThrow(() -> notFound("Компания не найдена"));
+ }
+}
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/resources/application.yml b/borschevskaya_anna_lab_3/company-service/src/main/resources/application.yml
new file mode 100644
index 0000000..1580336
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/resources/application.yml
@@ -0,0 +1,28 @@
+server:
+ port: ${SERVER_PORT:8080}
+spring:
+ application:
+ name: company-app
+ jpa:
+ database: POSTGRESQL
+ open-in-view: false
+ show-sql: false
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+ datasource:
+ url: ${DB_URL:jdbc:postgresql://postgres:5433/company}
+ username: ${DB_USERNAME:postgres}
+ password: ${DB_PASSWORD:postgres}
+ driverClassName: org.postgresql.Driver
+ type: com.zaxxer.hikari.HikariDataSource
+ hikari:
+ maximum-pool-size: 15
+ minimum-idle: 4
+ idle-timeout: 180000
+ max-lifetime: 599000
+ flyway:
+ enabled: true
+ locations: db/migration/
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/company-service/src/main/resources/db/migration/V000001__init.sql b/borschevskaya_anna_lab_3/company-service/src/main/resources/db/migration/V000001__init.sql
new file mode 100644
index 0000000..7704135
--- /dev/null
+++ b/borschevskaya_anna_lab_3/company-service/src/main/resources/db/migration/V000001__init.sql
@@ -0,0 +1,6 @@
+CREATE TABLE company (
+ id UUID
+ CONSTRAINT company_pk PRIMARY KEY,
+ name VARCHAR(255) UNIQUE NOT NULL,
+ address VARCHAR(255) NOT NULL
+);
diff --git a/borschevskaya_anna_lab_3/docker-compose.yml b/borschevskaya_anna_lab_3/docker-compose.yml
new file mode 100644
index 0000000..da5aa85
--- /dev/null
+++ b/borschevskaya_anna_lab_3/docker-compose.yml
@@ -0,0 +1,43 @@
+services:
+ postgres:
+ image: postgres:latest
+ container_name: postgres
+ environment:
+ POSTGRES_USERNAME: postgres
+ POSTGRES_PASSWORD: postgres
+ PGDATA: "/var/lib/postgresql/data/pgdata"
+ ports:
+ - "5432:5432"
+ volumes:
+ - ./postgres_data:/var/lib/postgresql/data/
+ - ./init-database.sh:/docker-entrypoint-initdb.d/init-database.sh
+ company:
+ build: ./company-service
+ container_name: company
+ depends_on:
+ - postgres
+ environment:
+ SERVER_PORT: 8080
+ DB_URL: jdbc:postgresql://postgres:5432/company
+ DB_USERNAME: postgres
+ DB_PASSWORD: postgres
+ vacancy:
+ build: ./vacancy-service
+ container_name: vacancy
+ depends_on:
+ - postgres
+ environment:
+ SERVER_PORT: 8080
+ DB_URL: jdbc:postgresql://postgres:5432/vacancy
+ DB_USERNAME: postgres
+ DB_PASSWORD: postgres
+ COMPANY_URL: http://nginx/
+ nginx:
+ image: nginx
+ depends_on:
+ - vacancy
+ - company
+ volumes:
+ - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
+ ports:
+ - 80:80
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/init-database.sh b/borschevskaya_anna_lab_3/init-database.sh
new file mode 100644
index 0000000..63e5d3a
--- /dev/null
+++ b/borschevskaya_anna_lab_3/init-database.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+
+# Создаем БД
+psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
+ CREATE DATABASE company;
+ CREATE DATABASE vacancy;
+EOSQL
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/nginx/nginx.conf b/borschevskaya_anna_lab_3/nginx/nginx.conf
new file mode 100644
index 0000000..4a27944
--- /dev/null
+++ b/borschevskaya_anna_lab_3/nginx/nginx.conf
@@ -0,0 +1,21 @@
+server {
+ listen 80;
+ listen [::]:80;
+ server_name localhost;
+
+ location /api/v1/company {
+ proxy_pass http://company:8080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location /api/v1/vacancy {
+ proxy_pass http://vacancy:8080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/vacancy-service/.gitignore b/borschevskaya_anna_lab_3/vacancy-service/.gitignore
new file mode 100644
index 0000000..5ff6309
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/.gitignore
@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/vacancy-service/Dockerfile b/borschevskaya_anna_lab_3/vacancy-service/Dockerfile
new file mode 100644
index 0000000..cc673eb
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/Dockerfile
@@ -0,0 +1,21 @@
+# Используем образ Maven для сборки
+FROM maven:3.8-eclipse-temurin-21-alpine AS build
+
+# Устанавливаем рабочую директорию
+WORKDIR /app
+
+# Копируем остальные исходные файлы
+COPY pom.xml .
+COPY src src
+
+# Собираем весь проект
+RUN mvn clean package -DskipTests
+
+# Используем официальный образ JDK для запуска собранного jar-файла
+FROM eclipse-temurin:21-jdk-alpine
+
+# Копируем jar-файл из предыдущего этапа
+COPY --from=build /app/target/*.jar /app.jar
+
+# Указываем команду для запуска приложения
+CMD ["java", "-jar", "app.jar"]
diff --git a/borschevskaya_anna_lab_3/vacancy-service/pom.xml b/borschevskaya_anna_lab_3/vacancy-service/pom.xml
new file mode 100644
index 0000000..e6374ce
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/pom.xml
@@ -0,0 +1,128 @@
+
+
+ 4.0.0
+
+ ru.somecompany
+ vacancy-service
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.3
+
+
+
+
+ 21
+ 21
+ UTF-8
+ 1.5.5.Final
+ 0.2.0
+ 2023.0.3
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.flywaydb
+ flyway-core
+
+
+ org.postgresql
+ postgresql
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ ${lombok-mapstruct-binding.version}
+
+
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+
+
+ io.swagger.core.v3
+ swagger-annotations
+ 2.2.1
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.3.0
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ ${spring-cloud.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ ${lombok-mapstruct-binding.version}
+
+
+
+ -Amapstruct.defaultComponentModel=spring
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/VacancyApplication.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/VacancyApplication.java
new file mode 100644
index 0000000..5c71f93
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/VacancyApplication.java
@@ -0,0 +1,14 @@
+package ru.somecompany;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+
+@SpringBootApplication
+@EnableFeignClients
+public class VacancyApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(VacancyApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyController.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyController.java
new file mode 100644
index 0000000..d260e55
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyController.java
@@ -0,0 +1,64 @@
+package ru.somecompany.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.constraints.NotNull;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import ru.somecompany.domain.CreateVacancyRequest;
+import ru.somecompany.domain.VacancyEntity;
+import ru.somecompany.domain.VacancyResponse;
+
+import java.util.List;
+import java.util.UUID;
+
+@Validated
+@Tag(name = "vacancy", description = "API для управления вакансиями")
+public interface VacancyController {
+
+ @Operation(summary = "Получение всех вакансий")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Данные успешно переданы"),
+ })
+ @GetMapping(value = "/api/v1/vacancy")
+ List getVacancies();
+
+ @Operation(summary = "Создание вакансии")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Вакансия успешно создана"),
+ })
+ @PostMapping(value = "/api/v1/vacancy")
+ VacancyEntity createVacancy(@RequestBody @NotNull CreateVacancyRequest request);
+
+ @Operation(summary = "Получение информации о вакансии по id")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Данные успешно переданы"),
+ @ApiResponse(responseCode = "404", description = "Вакансия не найдена")
+ })
+ @GetMapping(value = "/api/v1/vacancy/{vacancyId}")
+ VacancyResponse getVacancy(@PathVariable UUID vacancyId);
+
+ @Operation(summary = "Редактирование вакансии")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Данные вакансии успешно изменены"),
+ @ApiResponse(responseCode = "404", description = "Вакансия не найдена")
+ })
+ @PutMapping(value = "/api/v1/vacancy/{vacancyId}")
+ VacancyEntity updateVacancy(@PathVariable UUID vacancyId,
+ @RequestBody @NotNull CreateVacancyRequest request);
+
+ @Operation(summary = "Удалении вакансии по id")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Вакансия успешно удалена"),
+ @ApiResponse(responseCode = "404", description = "Вакансия не найдена")
+ })
+ @DeleteMapping(value = "/api/v1/vacancy/{vacancyId}")
+ void deleteVacancy(@PathVariable UUID vacancyId);
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyControllerImpl.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyControllerImpl.java
new file mode 100644
index 0000000..7713140
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/controller/VacancyControllerImpl.java
@@ -0,0 +1,45 @@
+package ru.somecompany.controller;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.RestController;
+import ru.somecompany.domain.CreateVacancyRequest;
+import ru.somecompany.domain.VacancyEntity;
+import ru.somecompany.domain.VacancyResponse;
+import ru.somecompany.service.VacancyService;
+
+import java.util.List;
+import java.util.UUID;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+public class VacancyControllerImpl implements VacancyController {
+
+ private final VacancyService vacancyService;
+
+ @Override
+ public List getVacancies() {
+ return vacancyService.getVacancies();
+ }
+
+ @Override
+ public VacancyEntity createVacancy(CreateVacancyRequest request) {
+ return vacancyService.createVacancy(request);
+ }
+
+ @Override
+ public VacancyResponse getVacancy(UUID vacancyId) {
+ return vacancyService.getVacancy(vacancyId);
+ }
+
+ @Override
+ public VacancyEntity updateVacancy(UUID vacancyId, CreateVacancyRequest request) {
+ return vacancyService.updateVacancy(vacancyId, request);
+ }
+
+ @Override
+ public void deleteVacancy(UUID vacancyId) {
+ vacancyService.deleteVacancy(vacancyId);
+ }
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CompanyResponse.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CompanyResponse.java
new file mode 100644
index 0000000..77fef37
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CompanyResponse.java
@@ -0,0 +1,17 @@
+package ru.somecompany.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.util.UUID;
+
+@Data
+@AllArgsConstructor
+public class CompanyResponse {
+
+ private UUID id;
+
+ private String name;
+
+ private String address;
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CreateVacancyRequest.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CreateVacancyRequest.java
new file mode 100644
index 0000000..1a35235
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/CreateVacancyRequest.java
@@ -0,0 +1,21 @@
+package ru.somecompany.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.util.UUID;
+
+@Data
+@AllArgsConstructor
+public class CreateVacancyRequest {
+
+ private String name;
+
+ private String description;
+
+ private Integer salaryLow;
+
+ private Integer salaryHigh;
+
+ private UUID companyId;
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyEntity.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyEntity.java
new file mode 100644
index 0000000..c4fb1c2
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyEntity.java
@@ -0,0 +1,41 @@
+package ru.somecompany.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+@Data
+@Entity
+@Builder
+@Table(name = "vacancy")
+@AllArgsConstructor
+@NoArgsConstructor
+public class VacancyEntity {
+
+ @Id
+ @GeneratedValue
+ private UUID id;
+
+ @Column(name = "name", nullable = false)
+ private String name;
+
+ @Column(name = "description", nullable = true, length = 1000)
+ private String description;
+
+ @Column(name = "salary_low", nullable = false)
+ private Integer salaryLow;
+
+ @Column(name = "salary_high", nullable = false)
+ private Integer salaryHigh;
+
+ @Column(name = "company_id", nullable = false)
+ private UUID companyId;
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyResponse.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyResponse.java
new file mode 100644
index 0000000..56bae2b
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/domain/VacancyResponse.java
@@ -0,0 +1,25 @@
+package ru.somecompany.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.UUID;
+
+@Data
+@Builder
+@Schema(description = "Данные по вакансии")
+public class VacancyResponse {
+
+ private UUID id;
+
+ private String name;
+
+ private String description;
+
+ private Integer salaryLow;
+
+ private Integer salaryHigh;
+
+ private CompanyResponse company;
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java
new file mode 100644
index 0000000..fc34e6f
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/AbstractWebExceptionHandler.java
@@ -0,0 +1,41 @@
+package ru.somecompany.exception;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+import java.time.LocalDateTime;
+
+import static org.springframework.http.HttpStatus.NOT_FOUND;
+
+@Slf4j
+public abstract class AbstractWebExceptionHandler {
+
+ public static ResponseEntity buildResponse(ErrorCode errorCode, HttpStatus status, String msg,
+ boolean details, Throwable ex) {
+ return buildResponse(errorCode.name(), status, msg, details, ex, LocalDateTime.now());
+ }
+
+ public static ResponseEntity buildResponse(String errorCode, HttpStatus status, String msg,
+ boolean details, Throwable ex, LocalDateTime timestamp) {
+ if (details) {
+ log.error(msg, ex); // with stack-trace
+ } else {
+ var message = StringUtils.equals(msg, ex.getMessage()) ? msg : msg + ": " + ex.getMessage();
+ if (status == NOT_FOUND) {
+ log.warn(message);
+ } else {
+ log.error(message);
+ }
+ }
+
+ return ResponseEntity.status(status.value())
+ .body(ResponseError.builder()
+ .code(errorCode)
+ .message(msg)
+ .timestamp(timestamp)
+ .status(status.value())
+ .build());
+ }
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/BasicError.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/BasicError.java
new file mode 100644
index 0000000..baca00a
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/BasicError.java
@@ -0,0 +1,5 @@
+package ru.somecompany.exception;
+
+public enum BasicError implements ErrorCode {
+ NOT_FOUND
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java
new file mode 100644
index 0000000..5148716
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/CompanyAppRuntimeException.java
@@ -0,0 +1,26 @@
+package ru.somecompany.exception;
+
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public class CompanyAppRuntimeException extends RuntimeException {
+ private final ErrorCode errorCode;
+ private final HttpStatus status;
+ private final boolean details;
+
+ public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message) {
+ this(errorCode, status, message, null);
+ }
+
+ public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message, Throwable error) {
+ this(errorCode, status, message, true, error);
+ }
+
+ public CompanyAppRuntimeException(ErrorCode errorCode, HttpStatus status, String message, boolean details, Throwable error) {
+ super(message, error);
+ this.errorCode = errorCode;
+ this.status = status;
+ this.details = details;
+ }
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ErrorCode.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ErrorCode.java
new file mode 100644
index 0000000..ed73439
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ErrorCode.java
@@ -0,0 +1,9 @@
+package ru.somecompany.exception;
+
+import java.io.Serializable;
+
+public interface ErrorCode extends Serializable {
+
+ String name();
+
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java
new file mode 100644
index 0000000..7ecd7c0
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResourceNotFoundException.java
@@ -0,0 +1,28 @@
+package ru.somecompany.exception;
+
+import org.springframework.http.HttpStatus;
+
+import static ru.somecompany.exception.BasicError.NOT_FOUND;
+
+public class ResourceNotFoundException extends CompanyAppRuntimeException {
+
+ public ResourceNotFoundException(ErrorCode errorCode, String message) {
+ this(errorCode, message, null);
+ }
+
+ public ResourceNotFoundException(ErrorCode errorCode, String message, Throwable error) {
+ super(errorCode, HttpStatus.NOT_FOUND, message, error);
+ }
+
+ public static ResourceNotFoundException notFound(String message, Object... args) {
+ return new ResourceNotFoundException(NOT_FOUND, String.format(message, args), null);
+ }
+
+ public static ResourceNotFoundException notFound() {
+ return new ResourceNotFoundException(NOT_FOUND, "Запрашиваемые данные не найдены", null);
+ }
+
+ public static ResourceNotFoundException notFound(Throwable error) {
+ return new ResourceNotFoundException(NOT_FOUND, error.getMessage(), error);
+ }
+}
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResponseError.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResponseError.java
new file mode 100644
index 0000000..4ae118d
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/ResponseError.java
@@ -0,0 +1,24 @@
+package ru.somecompany.exception;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ResponseError {
+ @JsonProperty(required = true)
+ private LocalDateTime timestamp;
+ @JsonProperty(required = true)
+ private Integer status;
+ @JsonProperty(required = true)
+ private String message;
+ @JsonProperty(required = true)
+ private String code;
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java
new file mode 100644
index 0000000..78fa51f
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/exception/WebExceptionHandler.java
@@ -0,0 +1,22 @@
+package ru.somecompany.exception;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@Getter
+@RestControllerAdvice
+@RequiredArgsConstructor
+public class WebExceptionHandler extends AbstractWebExceptionHandler {
+ private final ObjectMapper objectMapper;
+
+ @ExceptionHandler(CompanyAppRuntimeException.class)
+ public ResponseEntity handleException(CompanyAppRuntimeException ex) {
+ return buildResponse(ex.getErrorCode(), ex.getStatus(), ex.getMessage(), ex.isDetails(), ex);
+ }
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/external/CompanyClient.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/external/CompanyClient.java
new file mode 100644
index 0000000..17eed7b
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/external/CompanyClient.java
@@ -0,0 +1,15 @@
+package ru.somecompany.external;
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import ru.somecompany.domain.CompanyResponse;
+
+import java.util.UUID;
+
+@FeignClient(name = "company", url = "${app.feign.company-url}")
+public interface CompanyClient {
+
+ @GetMapping(value = "/api/v1/company/{companyId}")
+ CompanyResponse getCompany(@PathVariable UUID companyId);
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/mapper/VacancyMapper.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/mapper/VacancyMapper.java
new file mode 100644
index 0000000..2be7961
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/mapper/VacancyMapper.java
@@ -0,0 +1,24 @@
+package ru.somecompany.mapper;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.MappingTarget;
+import ru.somecompany.domain.CompanyResponse;
+import ru.somecompany.domain.CreateVacancyRequest;
+import ru.somecompany.domain.VacancyEntity;
+import ru.somecompany.domain.VacancyResponse;
+
+import java.util.UUID;
+
+@Mapper(imports = UUID.class)
+public interface VacancyMapper {
+
+ @Mapping(target = "company", source = "company")
+ @Mapping(target = "name", source = "entity.name")
+ @Mapping(target = "id", source = "entity.id")
+ VacancyResponse map(VacancyEntity entity, CompanyResponse company);
+
+ VacancyEntity map(CreateVacancyRequest request);
+
+ VacancyEntity map(@MappingTarget VacancyEntity entity, CreateVacancyRequest request);
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/repository/VacancyRepository.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/repository/VacancyRepository.java
new file mode 100644
index 0000000..5d68d48
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/repository/VacancyRepository.java
@@ -0,0 +1,11 @@
+package ru.somecompany.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+import ru.somecompany.domain.VacancyEntity;
+
+import java.util.UUID;
+
+@Repository
+public interface VacancyRepository extends JpaRepository {
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyService.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyService.java
new file mode 100644
index 0000000..0ea9def
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyService.java
@@ -0,0 +1,21 @@
+package ru.somecompany.service;
+
+import ru.somecompany.domain.CreateVacancyRequest;
+import ru.somecompany.domain.VacancyEntity;
+import ru.somecompany.domain.VacancyResponse;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface VacancyService {
+
+ List getVacancies();
+
+ VacancyEntity createVacancy(CreateVacancyRequest request);
+
+ VacancyEntity updateVacancy(UUID vacancyId, CreateVacancyRequest request);
+
+ void deleteVacancy(UUID vacancyId);
+
+ VacancyResponse getVacancy(UUID vacancyId);
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyServiceImpl.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyServiceImpl.java
new file mode 100644
index 0000000..db58bdd
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/VacancyServiceImpl.java
@@ -0,0 +1,68 @@
+package ru.somecompany.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import ru.somecompany.domain.CreateVacancyRequest;
+import ru.somecompany.domain.VacancyEntity;
+import ru.somecompany.domain.VacancyResponse;
+import ru.somecompany.mapper.VacancyMapper;
+import ru.somecompany.repository.VacancyRepository;
+import ru.somecompany.service.adapter.CompanyAdapter;
+
+import java.util.List;
+import java.util.UUID;
+
+import static ru.somecompany.exception.ResourceNotFoundException.notFound;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class VacancyServiceImpl implements VacancyService {
+
+ private final VacancyRepository vacancyRepository;
+ private final CompanyAdapter companyAdapter;
+ private final VacancyMapper mapper;
+
+ @Override
+ public List getVacancies() {
+ return vacancyRepository.findAll();
+ }
+
+ @Override
+ public VacancyEntity createVacancy(CreateVacancyRequest request) {
+ var entity = mapper.map(request);
+ return vacancyRepository.save(entity);
+ }
+
+ @Override
+ public VacancyEntity updateVacancy(UUID vacancyId, CreateVacancyRequest request) {
+ var entity = getById(vacancyId);
+ entity = mapper.map(entity, request);
+
+ return vacancyRepository.save(entity);
+ }
+
+ @Override
+ public void deleteVacancy(UUID vacancyId) {
+ var entity = getById(vacancyId);
+ vacancyRepository.delete(entity);
+ }
+
+ @Override
+ public VacancyResponse getVacancy(UUID vacancyId) {
+ var entity = getById(vacancyId);
+ var company = companyAdapter.getCompanyById(entity.getCompanyId());
+
+ return mapper.map(entity, company);
+ }
+
+ private VacancyEntity getById(UUID vacancyId) {
+ var entity = vacancyRepository.findById(vacancyId);
+ if (entity.isEmpty()) {
+ log.warn("The vacancy with id '{}' was not found", vacancyId);
+ throw notFound("Вакансия не найдена");
+ }
+ return entity.get();
+ }
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapter.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapter.java
new file mode 100644
index 0000000..1918a3c
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapter.java
@@ -0,0 +1,10 @@
+package ru.somecompany.service.adapter;
+
+import ru.somecompany.domain.CompanyResponse;
+
+import java.util.UUID;
+
+public interface CompanyAdapter {
+
+ CompanyResponse getCompanyById(UUID companyId);
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapterImpl.java b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapterImpl.java
new file mode 100644
index 0000000..ca18674
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/java/ru/somecompany/service/adapter/CompanyAdapterImpl.java
@@ -0,0 +1,20 @@
+package ru.somecompany.service.adapter;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import ru.somecompany.domain.CompanyResponse;
+import ru.somecompany.external.CompanyClient;
+
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class CompanyAdapterImpl implements CompanyAdapter {
+
+ private final CompanyClient companyClient;
+
+ @Override
+ public CompanyResponse getCompanyById(UUID companyId) {
+ return companyClient.getCompany(companyId);
+ }
+}
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/application.yml b/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/application.yml
new file mode 100644
index 0000000..afdc22e
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/application.yml
@@ -0,0 +1,32 @@
+server:
+ port: ${SERVER_PORT:8080}
+spring:
+ application:
+ name: vacancy-app
+ jpa:
+ database: POSTGRESQL
+ open-in-view: false
+ show-sql: false
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+ datasource:
+ url: ${DB_URL:jdbc:postgresql://localhost:5433/vacancy}
+ username: ${DB_USERNAME:postgres}
+ password: ${DB_PASSWORD:postgres}
+ driverClassName: org.postgresql.Driver
+ type: com.zaxxer.hikari.HikariDataSource
+ hikari:
+ maximum-pool-size: 15
+ minimum-idle: 4
+ idle-timeout: 180000
+ max-lifetime: 599000
+ flyway:
+ enabled: true
+ locations: db/migration/
+
+app:
+ feign:
+ company-url: ${COMPANY_URL:http://localhost:8081}
\ No newline at end of file
diff --git a/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/db/migration/V000001__init.sql b/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/db/migration/V000001__init.sql
new file mode 100644
index 0000000..983c6a9
--- /dev/null
+++ b/borschevskaya_anna_lab_3/vacancy-service/src/main/resources/db/migration/V000001__init.sql
@@ -0,0 +1,9 @@
+CREATE TABLE vacancy (
+ id UUID
+ CONSTRAINT company_pk PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ description VARCHAR(1000),
+ salary_low int8 NOT NULL,
+ salary_high int8 NOT NULL,
+ company_id UUID NOT NULL
+);