Compare commits

5 Commits

Author SHA1 Message Date
rrrrde1go
8f8aa1b8bb решение 2 этапа
Some checks failed
Android Test / validate-and-test (pull_request) Has been cancelled
2026-02-21 10:06:39 +03:00
c4afce7fa6 третья попытка(((( 2025-11-29 20:11:44 +03:00
0fc0678392 вторая попытка 2025-11-29 19:38:16 +03:00
d5564b63eb Задания Backend нто
Some checks failed
Android Test / validate-and-test (pull_request) Has been cancelled
2025-11-29 15:31:17 +03:00
a6954c2013 Update .gitea/workflows/workflow.yml 2025-11-24 17:17:06 +00:00
21 changed files with 369 additions and 93 deletions

View File

@@ -23,7 +23,7 @@ jobs:
GITEA_HEAD_REF: ${{ gitea.event.pull_request.head.ref }} GITEA_HEAD_REF: ${{ gitea.event.pull_request.head.ref }}
- name: Checkout tests - name: Checkout tests
run: python3 /opt/scripts/copy-tests.py --repo-url "Olympic/NTO-2025-Android-TeamTask-tests" --branch "main" --task-type "spring" run: python3 /opt/scripts/copy-tests.py --repo-url "Olympic/NTO-2025-Backend-TeamTask-tests" --branch "main" --task-type "spring"
- name: Run tests - name: Run tests
run: mvn test run: mvn test

View File

@@ -1,12 +1,11 @@
package com.example.nto; package com.example.nto;
/** import org.springframework.boot.SpringApplication;
* TODO: ДОРАБОТАТЬ в рамках задания import org.springframework.boot.autoconfigure.SpringBootApplication;
* =================================
* МОЖНО: Добавлять методы, аннотации, зависимости @SpringBootApplication
* НЕЛЬЗЯ: Изменять название класса и пакета
*/
public class App { public class App {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(App.class, args);
} }
} }

View File

@@ -1,10 +1,32 @@
package com.example.nto.controller; package com.example.nto.controller;
/** import com.example.nto.controller.dto.BookingCreateDto;
* TODO: ДОРАБОТАТЬ в рамках задания import com.example.nto.controller.dto.PlaceDto;
* ================================= import com.example.nto.service.BookingService;
* МОЖНО: Добавлять методы, аннотации, зависимости import lombok.RequiredArgsConstructor;
* НЕЛЬЗЯ: Изменять название класса и пакета import org.springframework.http.HttpStatus;
*/ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.*;
@Validated
@RestController
@RequestMapping("api")
@RequiredArgsConstructor
public class BookingController { public class BookingController {
private static BookingService bookingService;
@GetMapping("/{code}/booking")
@ResponseStatus(code = HttpStatus.OK)
public Map<LocalDate, List<PlaceDto>> getByDate(@PathVariable String code) {
return bookingService.getFreePLace(code);
}
@GetMapping("/{code}/book")
@ResponseStatus(code = HttpStatus.CREATED)
public void create(@PathVariable String code, @RequestBody BookingCreateDto bookingCreateDto) {
bookingService.create(code, bookingCreateDto);
}
} }

View File

@@ -1,10 +1,27 @@
package com.example.nto.controller; package com.example.nto.controller;
/** import com.example.nto.controller.dto.EmployeeDto;
* TODO: ДОРАБОТАТЬ в рамках задания import com.example.nto.service.EmployeeService;
* ================================= import lombok.RequiredArgsConstructor;
* МОЖНО: Добавлять методы, аннотации, зависимости import org.springframework.http.HttpStatus;
* НЕЛЬЗЯ: Изменять название класса и пакета import org.springframework.web.bind.annotation.*;
*/
@RestController
@RequestMapping("api")
@RequiredArgsConstructor
public class EmployeeController { public class EmployeeController {
private final EmployeeService employeeService;
@GetMapping("/{code}/auth")
@ResponseStatus(code = HttpStatus.OK)
public void login(@PathVariable String code) {
employeeService.auth(code);
}
@GetMapping("/{code}/info")
@ResponseStatus(code = HttpStatus.OK)
public EmployeeDto getByCode(@PathVariable String code) {
return employeeService.getByCode(code);
}
} }

View File

@@ -0,0 +1,18 @@
package com.example.nto.controller.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.*;
import java.time.LocalDate;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BookingCreateDto {
@NotNull
private LocalDate date;
@Positive
private long placeId;
}

View File

@@ -0,0 +1,32 @@
package com.example.nto.controller.dto;
import com.example.nto.entity.Booking;
import com.example.nto.entity.Employee;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.Map;
import java.util.TreeMap;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeDto {
private String name;
private String photoUrl;
private Map<LocalDate, PlaceDto> booking;
public static EmployeeDto toDto(Employee employee) {
Map<LocalDate, PlaceDto> dtoTreeMap = new TreeMap<>();
for (Booking booking : employee.getBookingList()) {
dtoTreeMap.put(booking.getDate(), PlaceDto.toDto(booking.getPlace()));
}
return new EmployeeDto(employee.getName(), employee.getPhotoUrl(), dtoTreeMap);
}
}

View File

@@ -0,0 +1,18 @@
package com.example.nto.controller.dto;
import com.example.nto.entity.Place;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PlaceDto {
private long id;
private String place;
public static PlaceDto toDto(Place place){return new PlaceDto(place.getId(), place.getPlace());}
}

View File

@@ -1,8 +1,6 @@
package com.example.nto.entity; package com.example.nto.entity;
import jakarta.persistence.FetchType; import jakarta.persistence.*;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@@ -10,26 +8,28 @@ import lombok.NoArgsConstructor;
import java.time.LocalDate; import java.time.LocalDate;
/**
* TODO: ДОРАБОТАТЬ в рамках задания
* =================================
* МОЖНО: Добавлять методы, аннотации, зависимости
* НЕЛЬЗЯ: Изменять название класса и пакета
*/
@Data @Data
@Entity
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Table(name = "booking")
public class Booking { public class Booking {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id; private long id;
@Column(name = "date")
private LocalDate date; private LocalDate date;
@ManyToOne(targetEntity = Place.class, fetch = FetchType.LAZY) @ManyToOne(targetEntity = Place.class, fetch = FetchType.LAZY)
@JoinColumn(name = "place_id") @JoinColumn(name = "place_id")
private Place place; private Place place;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "employee_id")
private Employee employee; private Employee employee;
} }

View File

@@ -8,27 +8,28 @@ import lombok.NoArgsConstructor;
import java.util.List; import java.util.List;
/**
* TODO: ДОРАБОТАТЬ в рамках задания
* =================================
* МОЖНО: Добавлять методы, аннотации, зависимости
* НЕЛЬЗЯ: Изменять название класса и пакета
*/
@Data @Data
@Entity
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Table(name = "employee")
public class Employee { public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id; private long id;
@Column(name = "name")
private String name; private String name;
@Column(name = "code")
private String code; private String code;
@Column(name = "photo_url")
private String photoUrl; private String photoUrl;
@OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Booking> bookingList; private List<Booking> bookingList;
} }

View File

@@ -1,29 +1,23 @@
package com.example.nto.entity; package com.example.nto.entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.*;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
/**
* TODO: ДОРАБОТАТЬ в рамках задания
* =================================
* МОЖНО: Добавлять методы, аннотации, зависимости
* НЕЛЬЗЯ: Изменять название класса и пакета
*/
@Data @Data
@Entity
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Table(name = "place")
public class Place { public class Place {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id; private long id;
@Column(name = "place_name")
private String place; private String place;
} }

View File

@@ -0,0 +1,7 @@
package com.example.nto.exception;
public class BookingAlreadyExistException extends RuntimeException {
public BookingAlreadyExistException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.example.nto.exception;
public class EmployeeNotFoundException extends RuntimeException {
public EmployeeNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.example.nto.exception;
public class PlaceNotFoundException extends RuntimeException {
public PlaceNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
package com.example.nto.exception.handler;
import com.example.nto.exception.BookingAlreadyExistException;
import com.example.nto.exception.EmployeeNotFoundException;
import com.example.nto.exception.PlaceNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EmployeeNotFoundException.class)
public ResponseEntity<String> handleEmployeeNotFoundException(EmployeeNotFoundException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED);
}
@ExceptionHandler(BookingAlreadyExistException.class)
public ResponseEntity<String> handleBookingAlreadyExistException(BookingAlreadyExistException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT);
}
@ExceptionHandler(PlaceNotFoundException.class)
public ResponseEntity<String> handlePlaceNotFoundException(PlaceNotFoundException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handlerGenericException(Exception e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
}

View File

@@ -1,10 +1,18 @@
package com.example.nto.repository; package com.example.nto.repository;
/** import com.example.nto.entity.Booking;
* TODO: ДОРАБОТАТЬ в рамках задания import com.example.nto.entity.Employee;
* ================================= import com.example.nto.entity.Place;
* МОЖНО: Добавлять методы, аннотации, зависимости import org.springframework.data.jpa.repository.JpaRepository;
* НЕЛЬЗЯ: Изменять название класса и пакета
*/ import java.time.LocalDate;
public interface BookingRepository { import java.util.List;
import java.util.Optional;
public interface BookingRepository extends JpaRepository<Booking, Long> {
List<Booking> findByDateBetween(LocalDate start, LocalDate end);
Optional<Booking> findByDateAndPlace(LocalDate date, Place place);
Optional<Booking> findByDateAndEmployee(LocalDate date, Employee employee);
} }

View File

@@ -1,10 +1,12 @@
package com.example.nto.repository; package com.example.nto.repository;
/** import com.example.nto.entity.Employee;
* TODO: ДОРАБОТАТЬ в рамках задания import org.springframework.data.jpa.repository.EntityGraph;
* ================================= import org.springframework.data.jpa.repository.JpaRepository;
* МОЖНО: Добавлять методы, аннотации, зависимости
* НЕЛЬЗЯ: Изменять название класса и пакета import java.util.Optional;
*/
public interface EmployeeRepository { public interface EmployeeRepository extends JpaRepository<Employee, Long> {
@EntityGraph(attributePaths = {"bookingList", "bookingList.place"})
Optional<Employee> findByCode(String code);
} }

View File

@@ -1,10 +1,7 @@
package com.example.nto.repository; package com.example.nto.repository;
/** import com.example.nto.entity.Place;
* TODO: ДОРАБОТАТЬ в рамках задания import org.springframework.data.jpa.repository.JpaRepository;
* =================================
* МОЖНО: Добавлять методы, аннотации, зависимости public interface PlaceRepository extends JpaRepository<Place, Long> {
* НЕЛЬЗЯ: Изменять название класса и пакета
*/
public interface PlaceRepository {
} }

View File

@@ -1,10 +1,15 @@
package com.example.nto.service; package com.example.nto.service;
/** import com.example.nto.controller.dto.BookingCreateDto;
* TODO: ДОРАБОТАТЬ в рамках задания import com.example.nto.controller.dto.PlaceDto;
* ================================= import com.example.nto.entity.Booking;
* МОЖНО: Добавлять методы, аннотации, зависимости
* НЕЛЬЗЯ: Изменять название класса и пакета import java.time.LocalDate;
*/ import java.util.List;
import java.util.Map;
public interface BookingService { public interface BookingService {
Map<LocalDate, List<PlaceDto>> getFreePLace(String code);
Booking create(String code, BookingCreateDto bookingCreateDto);
} }

View File

@@ -1,10 +1,8 @@
package com.example.nto.service; package com.example.nto.service;
/** import com.example.nto.controller.dto.EmployeeDto;
* TODO: ДОРАБОТАТЬ в рамках задания
* =================================
* МОЖНО: Добавлять методы, аннотации, зависимости
* НЕЛЬЗЯ: Изменять название класса и пакета
*/
public interface EmployeeService { public interface EmployeeService {
} EmployeeDto getByCode(String code);
void auth(String code);
}

View File

@@ -1,12 +1,102 @@
package com.example.nto.service.impl; package com.example.nto.service.impl;
import com.example.nto.controller.dto.BookingCreateDto;
import com.example.nto.controller.dto.PlaceDto;
import com.example.nto.entity.Booking;
import com.example.nto.entity.Employee;
import com.example.nto.entity.Place;
import com.example.nto.exception.BookingAlreadyExistException;
import com.example.nto.exception.EmployeeNotFoundException;
import com.example.nto.exception.PlaceNotFoundException;
import com.example.nto.repository.BookingRepository;
import com.example.nto.repository.EmployeeRepository;
import com.example.nto.repository.PlaceRepository;
import com.example.nto.service.BookingService; import com.example.nto.service.BookingService;
import com.example.nto.service.EmployeeService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** import java.time.LocalDate;
* TODO: ДОРАБОТАТЬ в рамках задания import java.time.ZoneId;
* ================================= import java.util.*;
* МОЖНО: Добавлять методы, аннотации, зависимости import java.util.stream.Collectors;
* НЕЛЬЗЯ: Изменять название класса и пакета
*/ @Service
@RequiredArgsConstructor
public class BookingServiceImpl implements BookingService { public class BookingServiceImpl implements BookingService {
private final BookingRepository bookingRepository;
private final EmployeeRepository employeeRepository;
private final PlaceRepository placeRepository;
private final EmployeeService employeeService;
@Value("${booking.days-ahead}")
private int daysAhead;
@Override
@Transactional(readOnly = true)
public Map<LocalDate, List<PlaceDto>> getFreePLace(String code) {
employeeService.auth(code);
List<Place> allPlaces = placeRepository.findAll();
LocalDate today = LocalDate.now(ZoneId.systemDefault());
LocalDate end = today.plusDays(daysAhead);
List<Booking> bookings = bookingRepository.findByDateBetween(today, end);
Map<LocalDate, Set<Long>> busyByDate = bookings.stream()
.collect(Collectors.groupingBy(Booking::getDate, Collectors.mapping(b -> b.getPlace().getId(), Collectors.toSet())));
Map<LocalDate, List<PlaceDto>> result = new LinkedHashMap<>();
for (int i = 0; i <= daysAhead; i++){
LocalDate currentDate = today.plusDays(i);
Set<Long> busyPlaces = busyByDate.getOrDefault(currentDate, Collections.emptySet());
List<PlaceDto> freePlaces = allPlaces.stream()
.filter(place -> busyPlaces.contains(place.getId()))
.map(place -> new PlaceDto(place.getId(), place.getPlace()))
.toList();
result.put(currentDate, freePlaces);
}
return result;
}
@Override
@Transactional
public Booking create(String code, BookingCreateDto bookingCreateDto) {
LocalDate date = bookingCreateDto.getDate();
LocalDate today = LocalDate.now(ZoneId.systemDefault());
if (date.isBefore(today) || date.isAfter(today.plusDays(daysAhead))) {
throw new IllegalArgumentException("Date is out of booking window");
}
Employee employee = employeeRepository.findByCode(code)
.orElseThrow(() -> new EmployeeNotFoundException("Employee with " + code + " code not found!"));
long placeId = bookingCreateDto.getPlaceId();
Place place = placeRepository.findById(placeId)
.orElseThrow(() -> new PlaceNotFoundException("Place with " + placeId + " id not found!"));
if (bookingRepository.findByDateAndPlace(date, place).isPresent()) {
throw new BookingAlreadyExistException("Booking already exists");
}
if (bookingRepository.findByDateAndEmployee(date, employee).isPresent()) {
throw new BookingAlreadyExistException("This employee already has another booking on " + date);
}
Booking booking = Booking.builder()
.date(date)
.employee(employee)
.place(place)
.build();
return bookingRepository.save(booking);
}
} }

View File

@@ -1,12 +1,31 @@
package com.example.nto.service.impl; package com.example.nto.service.impl;
import com.example.nto.controller.dto.EmployeeDto;
import com.example.nto.exception.EmployeeNotFoundException;
import com.example.nto.repository.EmployeeRepository;
import com.example.nto.service.EmployeeService; import com.example.nto.service.EmployeeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* TODO: ДОРАБОТАТЬ в рамках задания @Service
* ================================= @RequiredArgsConstructor
* МОЖНО: Добавлять методы, аннотации, зависимости
* НЕЛЬЗЯ: Изменять название класса и пакета
*/
public class EmployeeServiceImpl implements EmployeeService { public class EmployeeServiceImpl implements EmployeeService {
} private final EmployeeRepository employeeRepository;
@Override
@Transactional(readOnly = true)
public EmployeeDto getByCode(String code) {
return employeeRepository.findByCode(code).map(EmployeeDto::toDto)
.orElseThrow(() -> new EmployeeNotFoundException(("Employee with " + code + " code not found!")));
}
@Override
@Transactional(readOnly = true)
public void auth(String code) {
if (employeeRepository.findByCode(code).isEmpty()) {
throw new EmployeeNotFoundException("Employee with " + code + " code not found!");
}
}
}