diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 4c3baff..1309a1e 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -32,7 +32,8 @@ public class HandlerConfig { ViolationHandler violationHandler, ViolationStatisticsHandler violationStatisticsHandler, GlobalSearchHandler globalSearchHandler, - ViolationNotionHandler violationNotionHandler + ViolationNotionHandler violationNotionHandler, + ComplaintEntityHandler complaintEntityHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -56,6 +57,7 @@ public class HandlerConfig { map.put(30010, violationStatisticsHandler); map.put(30011, globalSearchHandler); map.put(30012, violationNotionHandler); + map.put(30013, complaintEntityHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index ec273ba..886c0b3 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -55,6 +55,7 @@ public enum MessageCode { "more than max valid"), USER_NOT_ACTIVE(2, "User not active"), NOTION_NOT_FOUND(4, "Notion not found"), + NOT_FOUND(4, "Notion not found"), MESSAGE_IS_REQUIRED_FOR_NOTION(4, "Message is required for notion"); private final Integer code; diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java new file mode 100644 index 0000000..7c7d695 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java @@ -0,0 +1,41 @@ +package ru.soune.nocopy.dto.complaint; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import java.util.List; + +@Data +public class ComplaintRequest { + @JsonProperty("action") + private String action; + + @JsonProperty("id") + private Long id; + + @JsonProperty("violation_id") + private Long violationId; + + @JsonProperty("text") + private String complaintText; + + @JsonProperty("email") + private String email; + + @JsonProperty("links") + private List links; + + @JsonProperty("status") + private String status; + + @JsonProperty("page") + private Integer page; + + @JsonProperty("size") + private Integer size; + + @JsonProperty("sort_by") + private String sortBy; + + @JsonProperty("sort_direction") + private String sortDirection; +} diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java new file mode 100644 index 0000000..4b86313 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java @@ -0,0 +1,39 @@ +package ru.soune.nocopy.dto.complaint; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ComplaintResponse { + @JsonProperty("id") + private Long id; + + @JsonProperty("status") + private String status; + + @JsonProperty("complaint_text") + private String complaintText; + + @JsonProperty("violation_id") + private Long violationId; + + @JsonProperty("email") + private String email; + + @JsonProperty("links") + private List links; + + @JsonProperty("created_at") + private String createdAt; + + @JsonProperty("updated_at") + private String updatedAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java new file mode 100644 index 0000000..d09a433 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java @@ -0,0 +1,58 @@ +package ru.soune.nocopy.entity.complaint; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.violation.Violation; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "complaint") +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter @Setter +@EntityListeners(AuditingEntityListener.class) +public class ComplaintEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "status") + @Enumerated(EnumType.STRING) + private ComplaintStatus status; + + @Column(name = "complaint_text", columnDefinition = "TEXT") + private String complaintText; + + @Column(name = "email") + private String email; + + @Column(name = "links", columnDefinition = "TEXT") + private String links; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "violation_id") + @ToString.Exclude + @JsonIgnore + private Violation violation; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "law_case_id") + @ToString.Exclude + @JsonIgnore + private LawCase lawCase; + + @Column(name = "created_at", updatable = false) + @CreatedDate + private LocalDateTime createdAt; + + @Column(name = "updated_at") + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java new file mode 100644 index 0000000..e65a83c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java @@ -0,0 +1,5 @@ +package ru.soune.nocopy.entity.complaint; + +public enum ComplaintStatus { + CREATED, IN_WORK, COMPLETED, CANCELLED +} diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java b/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java new file mode 100644 index 0000000..b9ca6e6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.entity.complaint; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter @Setter +@Table(name = "law_case") +public class LawCase { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; +} diff --git a/src/main/java/ru/soune/nocopy/exception/ComplaintNotFoundException.java b/src/main/java/ru/soune/nocopy/exception/ComplaintNotFoundException.java new file mode 100644 index 0000000..fce3a9f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/ComplaintNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class ComplaintNotFoundException extends RuntimeException { + public ComplaintNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/DuplicateComplaintException.java b/src/main/java/ru/soune/nocopy/exception/DuplicateComplaintException.java new file mode 100644 index 0000000..9ce0be5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/DuplicateComplaintException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class DuplicateComplaintException extends RuntimeException { + public DuplicateComplaintException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java new file mode 100644 index 0000000..a046205 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java @@ -0,0 +1,137 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.complaint.ComplaintRequest; +import ru.soune.nocopy.dto.complaint.ComplaintResponse; +import ru.soune.nocopy.entity.complaint.ComplaintStatus; +import ru.soune.nocopy.service.complaint.ComplaintEntityService; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ComplaintEntityHandler implements RequestHandler { + private final ComplaintEntityService complaintService; + + private final ObjectMapper objectMapper; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + Integer msgId = request.getMsgId(); + ComplaintRequest complaintRequest = objectMapper.convertValue(request.getMessageBody(), ComplaintRequest.class); + String action = complaintRequest.getAction(); + + try { + switch (action.toLowerCase()) { + case "create": + return handleCreate(msgId, complaintRequest); + case "get": + return handleGet(msgId, complaintRequest); + case "get_all": + return handleGetAll(msgId, complaintRequest); + case "get_by_violation": + return handleGetByViolation(msgId, complaintRequest); + case "update_status": + return handleUpdateStatus(msgId, complaintRequest); + case "update": + return handleUpdate(msgId, complaintRequest); + case "delete": + return handleDelete(msgId, complaintRequest); + default: + return errorResponse(msgId, MessageCode.MSG_ID_NOT_FOUND.getCode(), "Unknown action: " + action); + } + } catch (Exception e) { + log.error("Error handling complaint request", e); + return errorResponse(msgId, MessageCode.INVALID_JSON_BODY.getCode(), e.getMessage()); + } + } + + private BaseResponse handleCreate(Integer msgId, ComplaintRequest req) { + if (req.getViolationId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), + "violation_id is required"); + if (req.getComplaintText() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), + "text is required"); + + ComplaintResponse response = complaintService.createComplaint(req); + return successResponse(msgId, "Complaint created successfully", response); + } + + private BaseResponse handleGet(Integer msgId, ComplaintRequest req) { + if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required"); + + ComplaintResponse response = complaintService.getComplaintById(req.getId()); + return successResponse(msgId, "Complaint retrieved successfully", response); + } + + private BaseResponse handleGetAll(Integer msgId, ComplaintRequest req) { + Pageable pageable = PageRequest.of( + req.getPage() != null ? req.getPage() : 0, + req.getSize() != null ? req.getSize() : 20, + Sort.by(Sort.Direction.fromString(req.getSortDirection() != null ? req.getSortDirection() : "desc"), + req.getSortBy() != null ? req.getSortBy() : "createdAt") + ); + + var page = complaintService.getAllComplaints(pageable); + Map data = new HashMap<>(); + data.put("content", page.getContent()); + data.put("page", page.getNumber()); + data.put("size", page.getSize()); + data.put("totalElements", page.getTotalElements()); + data.put("totalPages", page.getTotalPages()); + + return successResponse(msgId, "Complaints retrieved successfully", data); + } + + private BaseResponse handleGetByViolation(Integer msgId, ComplaintRequest req) { + if (req.getViolationId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), + "violation_id is required"); + + var complaints = complaintService.getComplaintsByViolationId(req.getViolationId()); + Map data = new HashMap<>(); + data.put("content", complaints); + data.put("totalElements", complaints.size()); + + return successResponse(msgId, "Complaints retrieved successfully", data); + } + + private BaseResponse handleUpdateStatus(Integer msgId, ComplaintRequest req) { + if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required"); + if (req.getStatus() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "status is required"); + + ComplaintStatus status = ComplaintStatus.valueOf(req.getStatus().toUpperCase()); + ComplaintResponse response = complaintService.updateComplaintStatus(req.getId(), status); + return successResponse(msgId, "Status updated successfully", response); + } + + private BaseResponse handleUpdate(Integer msgId, ComplaintRequest req) { + if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required"); + + ComplaintResponse response = complaintService.updateComplaint(req.getId(), req); + return successResponse(msgId, "Complaint updated successfully", response); + } + + private BaseResponse handleDelete(Integer msgId, ComplaintRequest req) { + if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required"); + + ComplaintResponse response = complaintService.deleteComplaint(req.getId()); + return successResponse(msgId, "Complaint deleted successfully", response); + } + + private BaseResponse successResponse(Integer msgId, String message, Object data) { + return new BaseResponse(msgId, MessageCode.SUCCESS.getCode(), message, data); + } + + private BaseResponse errorResponse(Integer msgId, Integer code, String message) { + return new BaseResponse(msgId, code, message, new HashMap<>()); + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java new file mode 100644 index 0000000..159d388 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java @@ -0,0 +1,28 @@ +package ru.soune.nocopy.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.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.complaint.ComplaintEntity; +import ru.soune.nocopy.entity.complaint.ComplaintStatus; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ComplaintEntityRepository extends JpaRepository { + Optional findById(Long id); + Page findAll(Pageable pageable); + Page findByStatus(ComplaintStatus status, Pageable pageable); + List findByViolationId(Long violationId); + boolean existsByViolationId(Long violationId); + + @Modifying + @Transactional + @Query("UPDATE ComplaintEntity c SET c.status = :status WHERE c.id = :id") + int updateStatus(Long id, ComplaintStatus status); +} diff --git a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java new file mode 100644 index 0000000..fafc277 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java @@ -0,0 +1,148 @@ +package ru.soune.nocopy.service.complaint; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +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.soune.nocopy.dto.complaint.ComplaintRequest; +import ru.soune.nocopy.dto.complaint.ComplaintResponse; +import ru.soune.nocopy.entity.complaint.ComplaintEntity; +import ru.soune.nocopy.entity.complaint.ComplaintStatus; +import ru.soune.nocopy.entity.violation.Violation; +import ru.soune.nocopy.exception.ComplaintNotFoundException; +import ru.soune.nocopy.exception.DuplicateComplaintException; +import ru.soune.nocopy.exception.ViolationNotFoundException; +import ru.soune.nocopy.repository.ComplaintEntityRepository; +import ru.soune.nocopy.repository.ViolationRepository; + +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class ComplaintEntityService { + private final ComplaintEntityRepository complaintRepository; + + private final ViolationRepository violationRepository; + + private final ObjectMapper objectMapper; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + public ComplaintResponse createComplaint(ComplaintRequest request) { + Violation violation = violationRepository.findById(request.getViolationId()) + .orElseThrow(() -> new ViolationNotFoundException("Violation not found: " + request.getViolationId())); + + if (complaintRepository.existsByViolationId(request.getViolationId())) { + throw new DuplicateComplaintException("Complaint already exists for violation: " + request.getViolationId()); + } + + String linksJson = null; + if (request.getLinks() != null && !request.getLinks().isEmpty()) { + try { + linksJson = objectMapper.writeValueAsString(request.getLinks()); + } catch (JsonProcessingException e) { + linksJson = request.getLinks().toString(); + } + } + + ComplaintEntity complaint = ComplaintEntity.builder() + .status(ComplaintStatus.CREATED) + .complaintText(request.getComplaintText()) + .email(request.getEmail()) + .links(linksJson) + .violation(violation) + .build(); + + return mapToResponse(complaintRepository.save(complaint)); + } + + @Transactional(readOnly = true) + public ComplaintResponse getComplaintById(Long id) { + return complaintRepository.findById(id) + .map(this::mapToResponse) + .orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id)); + } + + @Transactional(readOnly = true) + public Page getAllComplaints(Pageable pageable) { + return complaintRepository.findAll(pageable).map(this::mapToResponse); + } + + @Transactional(readOnly = true) + public List getComplaintsByViolationId(Long violationId) { + if (!violationRepository.existsById(violationId)) { + throw new ViolationNotFoundException("Violation not found: " + violationId); + } + return complaintRepository.findByViolationId(violationId) + .stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + } + + public ComplaintResponse updateComplaintStatus(Long id, ComplaintStatus status) { + ComplaintEntity complaint = complaintRepository.findById(id) + .orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id)); + complaint.setStatus(status); + return mapToResponse(complaintRepository.save(complaint)); + } + + public ComplaintResponse updateComplaint(Long id, ComplaintRequest request) { + ComplaintEntity complaint = complaintRepository.findById(id) + .orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id)); + + if (request.getViolationId() != null) { + Violation violation = violationRepository.findById(request.getViolationId()) + .orElseThrow(() -> new ViolationNotFoundException("Violation not found: " + request.getViolationId())); + complaint.setViolation(violation); + } + if (request.getComplaintText() != null) complaint.setComplaintText(request.getComplaintText()); + if (request.getEmail() != null) complaint.setEmail(request.getEmail()); + if (request.getLinks() != null) { + try { + complaint.setLinks(objectMapper.writeValueAsString(request.getLinks())); + } catch (JsonProcessingException e) { + complaint.setLinks(request.getLinks().toString()); + } + } + + return mapToResponse(complaintRepository.save(complaint)); + } + + public ComplaintResponse deleteComplaint(Long id) { + ComplaintEntity complaint = complaintRepository.findById(id) + .orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id)); + complaint.setStatus(ComplaintStatus.CANCELLED); + return mapToResponse(complaintRepository.save(complaint)); + } + + private ComplaintResponse mapToResponse(ComplaintEntity entity) { + List linksList = null; + if (entity.getLinks() != null) { + try { + linksList = objectMapper.readValue(entity.getLinks(), + objectMapper.getTypeFactory().constructCollectionType(List.class, String.class)); + } catch (JsonProcessingException e) { + log.warn("Failed to parse links JSON: {}", entity.getLinks()); + } + } + + return ComplaintResponse.builder() + .id(entity.getId()) + .status(entity.getStatus() != null ? entity.getStatus().name() : null) + .complaintText(entity.getComplaintText()) + .email(entity.getEmail()) + .links(linksList) + .violationId(entity.getViolation() != null ? entity.getViolation().getId() : null) + .createdAt(entity.getCreatedAt() != null ? entity.getCreatedAt().format(DATE_FORMATTER) : null) + .updatedAt(entity.getUpdatedAt() != null ? entity.getUpdatedAt().format(DATE_FORMATTER) : null) + .build(); + } +}