Спроектировать REST Controller, создающий новый договор

11. Спроектировать и реализовать REST API для управления договорами

Описание
Спроектировать REST Controller, который:

  • создаёт новый договор
  • возвращает договор по номеру договора

В описании задачи предоставить модель Contract.

Модель Contract:

public class Contract {
    private Long id;
    private String number;     // номер договора (уникальный)
    private String clientId;   // идентификатор клиента
    private String status;     // например: NEW, ACTIVE, CLOSED
    private java.time.Instant createdAt;

    // getters/setters/constructors
}
Спойлеры к решению
Подсказки
💡 Для создания договора обычно используют POST /api/contracts.
💡 Получение договора по номеру удобно сделать как GET /api/contracts/{number}.
💡 Лучше принимать на вход ContractCreateDto, а наружу отдавать ContractResponseDto.
💡 Номер договора стоит валидировать (@NotBlank) и сделать уникальным (если есть БД — unique constraint).
💡 Если договор не найден — возвращать 404 Not Found.
Решение

DTO:

import jakarta.validation.constraints.NotBlank;

public class ContractCreateDto {
    @NotBlank
    private String number;

    @NotBlank
    private String clientId;

    // getters/setters
}
import java.time.Instant;

public class ContractResponseDto {
    private Long id;
    private String number;
    private String clientId;
    private String status;
    private Instant createdAt;

    // getters/setters/constructors
}

Service (in-memory пример):

import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Service
public class ContractService {
    private final AtomicLong idSeq = new AtomicLong(0);
    private final Map<String, Contract> byNumber = new ConcurrentHashMap<>();

    public Contract create(ContractCreateDto dto) {
        if (byNumber.containsKey(dto.getNumber())) {
            throw new IllegalArgumentException("Contract number already exists: " + dto.getNumber());
        }

        Contract contract = new Contract();
        contract.setId(idSeq.incrementAndGet());
        contract.setNumber(dto.getNumber());
        contract.setClientId(dto.getClientId());
        contract.setStatus("NEW");
        contract.setCreatedAt(Instant.now());

        byNumber.put(contract.getNumber(), contract);
        return contract;
    }

    public Contract getByNumber(String number) {
        Contract c = byNumber.get(number);
        if (c == null) {
            throw new NoSuchElementException("Contract not found: " + number);
        }
        return c;
    }
}

Controller:

import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.NoSuchElementException;

@RestController
@RequestMapping("/api/contracts")
public class ContractController {

    private final ContractService service;

    public ContractController(ContractService service) {
        this.service = service;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ContractResponseDto create(@Valid @RequestBody ContractCreateDto dto) {
        Contract c = service.create(dto);
        return toResponse(c);
    }

    @GetMapping("/{number}")
    public ContractResponseDto getByNumber(@PathVariable String number) {
        Contract c = service.getByNumber(number);
        return toResponse(c);
    }

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(NoSuchElementException.class)
    public String notFound(NoSuchElementException e) {
        return e.getMessage();
    }

    @ResponseStatus(HttpStatus.CONFLICT)
    @ExceptionHandler(IllegalArgumentException.class)
    public String conflict(IllegalArgumentException e) {
        return e.getMessage();
    }

    private static ContractResponseDto toResponse(Contract c) {
        ContractResponseDto dto = new ContractResponseDto();
        dto.setId(c.getId());
        dto.setNumber(c.getNumber());
        dto.setClientId(c.getClientId());
        dto.setStatus(c.getStatus());
        dto.setCreatedAt(c.getCreatedAt());
        return dto;
    }
}

.http ручные тесты:

contracts-create.http

POST http://localhost:8080/api/contracts
Content-Type: application/json

{
  "number": "CNTR-2026-0001",
  "clientId": "client-123"
}

contracts-get.http

GET http://localhost:8080/api/contracts/CNTR-2026-0001

Ключевые моменты:

  1. POST /api/contracts создаёт договор и возвращает 201 Created.

  2. GET /api/contracts/{number} возвращает договор по номеру, иначе 404.

  3. DTO отделяют вход/выход от внутренней модели.

  4. Проверка уникальности номера даёт 409 Conflict.