Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

198 add user complaints functionality #146

Merged
merged 2 commits into from
Jun 8, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -181,6 +181,19 @@
<artifactId>spring-test</artifactId>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.petmarket.users.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.petmarket.users.dto.ComplaintResponseDto;
import org.petmarket.users.entity.ComplaintStatus;
import org.petmarket.users.service.ComplaintService;
import org.petmarket.utils.annotations.parametrs.ParameterPageNumber;
import org.petmarket.utils.annotations.parametrs.ParameterPageSize;
import org.petmarket.utils.annotations.responses.ApiResponseForbidden;
import org.petmarket.utils.annotations.responses.ApiResponseNotFound;
import org.petmarket.utils.annotations.responses.ApiResponseSuccessful;
import org.petmarket.utils.annotations.responses.ApiResponseUnauthorized;
import org.springframework.data.domain.Sort;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "Complaints", description = "the user complaints API")
@Slf4j
@RequiredArgsConstructor
@RestController
@Validated
@RequestMapping(value = "/v1/admin/complaints")
public class ComplaintsAdminController {
private final ComplaintService complaintService;

@Operation(summary = "Delete complaint")
@ApiResponseSuccessful
@ApiResponseUnauthorized
@ApiResponseForbidden
@PreAuthorize("isAuthenticated()")
@DeleteMapping("/{id}")
public void deleteComplaint(@PathVariable Long id) {
log.info("Deleting complaint");
complaintService.deleteComplaint(id);
}

@Operation(summary = "Get complaint")
@ApiResponseSuccessful
@ApiResponseNotFound
@ApiResponseUnauthorized
@ApiResponseForbidden
@PreAuthorize("isAuthenticated()")
@GetMapping("/{id}")
public ComplaintResponseDto getComplaint(@PathVariable Long id) {
log.info("Getting complaint");
return complaintService.getComplaint(id);
}

@Operation(summary = "Get complaints")
@ApiResponseSuccessful
@ApiResponseUnauthorized
@ApiResponseForbidden
@PreAuthorize("isAuthenticated()")
@GetMapping
public List<ComplaintResponseDto> getComplaints(
@RequestParam(required = false, defaultValue = "PENDING") ComplaintStatus complaintStatus,
@ParameterPageNumber @RequestParam(defaultValue = "1") @Positive int page,
@ParameterPageSize @RequestParam(defaultValue = "30") @Positive int size,
@RequestParam(defaultValue = "DESC") Sort.Direction direction) {
log.info("Getting complaints");
return complaintService.getComplaints(complaintStatus, size, page, direction);
}

@Operation(summary = "Get complaints by user id")
@ApiResponseSuccessful
@ApiResponseUnauthorized
@ApiResponseForbidden
@PreAuthorize("isAuthenticated()")
@GetMapping("/user/{userId}")
public List<ComplaintResponseDto> getComplaintsByUserId(
@PathVariable Long userId,
@RequestParam(required = false, defaultValue = "PENDING") ComplaintStatus status,
@ParameterPageNumber @RequestParam(defaultValue = "1") @Positive int page,
@ParameterPageSize @RequestParam(defaultValue = "30") @Positive int size,
@RequestParam(defaultValue = "DESC") Sort.Direction direction) {
log.info("Getting complaints by user id");
return complaintService.getComplaintsByUserId(userId, status, size, page, direction);
}

@Operation(summary = "Update complaint status")
@ApiResponseSuccessful
@ApiResponseUnauthorized
@ApiResponseForbidden
@PreAuthorize("isAuthenticated()")
@PutMapping("/{id}")
public void updateComplaintStatus(@PathVariable Long id,
@RequestParam(defaultValue = "RESOLVED") ComplaintStatus status) {
log.info("Updating complaint status");
complaintService.updateStatusById(id, status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.petmarket.users.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.petmarket.users.dto.ComplaintRequestDto;
import org.petmarket.users.service.ComplaintService;
import org.petmarket.utils.annotations.responses.ApiResponseBadRequest;
import org.petmarket.utils.annotations.responses.ApiResponseSuccessful;
import org.petmarket.utils.annotations.responses.ApiResponseUnauthorized;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Tag(name = "Complaints", description = "the user complaints API")
@Slf4j
@RequiredArgsConstructor
@RestController
@Validated
@RequestMapping(value = "/v1/complaints")
public class ComplaintsController {
private final ComplaintService complaintService;

@Operation(summary = "Add complaint")
@ApiResponseSuccessful
@ApiResponseUnauthorized
@ApiResponseBadRequest
@PreAuthorize("isAuthenticated()")
@PostMapping
public void addComplaint(@RequestBody @Valid ComplaintRequestDto complaintRequestDto) {
log.info("Adding complaint");
complaintService.addComplaint(complaintRequestDto);
}
}
25 changes: 25 additions & 0 deletions src/main/java/org/petmarket/users/dto/ComplaintRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.petmarket.users.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.*;
import org.springframework.validation.annotation.Validated;

@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Validated
public class ComplaintRequestDto {
@NotBlank(message = "The 'text' cannot be empty")
@Schema(example = "Hello, how are you?")
@Size(max = 10000, message = "The 'text' length must be less than or equal to 10000")
private String complaint;

@Schema(example = "1")
@JsonProperty("complained_user_id")
private Long complainedUserId;
}
20 changes: 20 additions & 0 deletions src/main/java/org/petmarket/users/dto/ComplaintResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.petmarket.users.dto;

import lombok.*;

import java.util.Date;

@Setter
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ComplaintResponseDto {
private Long id;
private String complaint;
private Long userId;
private Long complainedUserId;
private String status;
private Date created;
private Date updated;
}
47 changes: 47 additions & 0 deletions src/main/java/org/petmarket/users/entity/Complaint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.petmarket.users.entity;

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 java.util.Date;

@Entity
@Table(name = "user_complaints")
@EqualsAndHashCode(of = {"id"})
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class Complaint {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "complaint")
private String complaint;

@ManyToOne
@JoinColumn(name = "user_id")
private User user;

@ManyToOne
@JoinColumn(name = "complained_user_id")
private User complainedUser;

@Enumerated(EnumType.STRING)
@Column(name = "status")
private ComplaintStatus status;

@CreatedDate
@Column(name = "created")
private Date created;

@LastModifiedDate
@Column(name = "updated")
private Date updated;
}
7 changes: 7 additions & 0 deletions src/main/java/org/petmarket/users/entity/ComplaintStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.petmarket.users.entity;

public enum ComplaintStatus {
PENDING,
RESOLVED,
REJECTED
}
21 changes: 21 additions & 0 deletions src/main/java/org/petmarket/users/mapper/ComplaintMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.petmarket.users.mapper;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.petmarket.config.MapperConfig;
import org.petmarket.users.dto.ComplaintRequestDto;
import org.petmarket.users.dto.ComplaintResponseDto;
import org.petmarket.users.entity.Complaint;

import java.util.List;

@Mapper(config = MapperConfig.class)
public interface ComplaintMapper {
Complaint mapDtoToComplaint(ComplaintRequestDto complaintRequestDto);

@Mapping(target = "userId", source = "user.id")
@Mapping(target = "complainedUserId", source = "complainedUser.id")
ComplaintResponseDto mapComplaintToDto(Complaint complaint);

List<ComplaintResponseDto> mapComplaintToDto(List<Complaint> complaints);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.petmarket.users.repository;

import org.petmarket.users.entity.Complaint;
import org.petmarket.users.entity.ComplaintStatus;
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.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface ComplaintRepository extends JpaRepository<Complaint, Long> {
Page<Complaint> findAllByComplainedUserIdAndStatus(Long userId, ComplaintStatus complaintStatus, Pageable pageable);

Page<Complaint> findAllByStatus(ComplaintStatus complaintStatus, Pageable pageable);

@Modifying
@Query("UPDATE Complaint c SET c.status = :status WHERE c.id = :id")
void updateStatusById(@Param("id") Long id, @Param("status") ComplaintStatus status);
}
65 changes: 65 additions & 0 deletions src/main/java/org/petmarket/users/service/ComplaintService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.petmarket.users.service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.petmarket.users.dto.ComplaintRequestDto;
import org.petmarket.users.dto.ComplaintResponseDto;
import org.petmarket.users.entity.Complaint;
import org.petmarket.users.entity.ComplaintStatus;
import org.petmarket.users.mapper.ComplaintMapper;
import org.petmarket.users.repository.ComplaintRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Service
public class ComplaintService {
private final ComplaintRepository complaintRepository;
private final ComplaintMapper complaintMapper;
private final UserService userService;

@Transactional
public void addComplaint(ComplaintRequestDto complaintRequestDto) {
if (complaintRequestDto.getComplainedUserId().equals(UserService.getCurrentUserId())) {
log.error("User cannot complain about himself");
throw new IllegalArgumentException("User cannot complain about himself");
}

Complaint complaint = complaintMapper.mapDtoToComplaint(complaintRequestDto);
complaint.setUser(userService.getCurrentUser());
complaint.setComplainedUser(userService.findById(complaintRequestDto.getComplainedUserId()));
complaint.setStatus(ComplaintStatus.PENDING);
complaintRepository.save(complaint);
}

public void deleteComplaint(Long id) {
complaintRepository.deleteById(id);
}

public ComplaintResponseDto getComplaint(Long id) {
return complaintMapper.mapComplaintToDto(complaintRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Complaint not found")));
}

public List<ComplaintResponseDto> getComplaints(ComplaintStatus complaintStatus, int size,
int page, Sort.Direction direction) {
return complaintMapper.mapComplaintToDto(complaintRepository.findAllByStatus(complaintStatus,
PageRequest.of(page - 1, size, Sort.by(direction, "created"))).toList());
}

public List<ComplaintResponseDto> getComplaintsByUserId(Long userId, ComplaintStatus status,
int size, int page, Sort.Direction direction) {
return complaintMapper.mapComplaintToDto(complaintRepository.findAllByComplainedUserIdAndStatus(userId,
status, PageRequest.of(page - 1, size, Sort.by(direction, "created"))).toList());
}

@Transactional
public void updateStatusById(Long id, ComplaintStatus status) {
complaintRepository.updateStatusById(id, status);
}
}
11 changes: 11 additions & 0 deletions src/main/resources/db/migration/V2024.06.02.001__add_complaint.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE user_complaints (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
complaint TEXT NOT NULL,
user_id BIGINT,
complained_user_id BIGINT,
status VARCHAR(255) NOT NULL DEFAULT 'PENDING',
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (complained_user_id) REFERENCES users(id)
);
182 changes: 182 additions & 0 deletions src/test/java/org/petmarket/users/service/ComplaintServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package org.petmarket.users.service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import org.petmarket.users.dto.ComplaintRequestDto;
import org.petmarket.users.dto.ComplaintResponseDto;
import org.petmarket.users.entity.Complaint;
import org.petmarket.users.entity.ComplaintStatus;
import org.petmarket.users.entity.User;
import org.petmarket.users.mapper.ComplaintMapper;
import org.petmarket.users.repository.ComplaintRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

@ExtendWith(MockitoExtension.class)
class ComplaintServiceTest {
@Mock
private ComplaintRepository complaintRepository;

@Mock
private ComplaintMapper complaintMapper;

@Mock
private UserService userService;

@InjectMocks
private ComplaintService complaintService;

@Test
public void testAddComplaintSelfComplaintThrowsException() {
try (MockedStatic<UserService> mockedUserService = mockStatic(UserService.class)) {
// Arrange
ComplaintRequestDto dto = new ComplaintRequestDto();
dto.setComplainedUserId(1L);

mockedUserService.when(UserService::getCurrentUserId).thenReturn(1L);

// Act
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> complaintService.addComplaint(dto));

// Assert
assertEquals("User cannot complain about himself", exception.getMessage());
}
}

@Test
public void testAddComplaintSuccess() {
try (MockedStatic<UserService> mockedUserService = mockStatic(UserService.class)) {
// Arrange
ComplaintRequestDto dto = new ComplaintRequestDto();
dto.setComplainedUserId(2L);

Complaint complaint = new Complaint();
mockedUserService.when(UserService::getCurrentUserId).thenReturn(1L);
when(userService.getCurrentUser()).thenReturn(new User());
when(complaintMapper.mapDtoToComplaint(dto)).thenReturn(complaint);

// Act
complaintService.addComplaint(dto);

// Assert
verify(complaintRepository, times(1)).save(complaint);
}
}

@Test
public void testDeleteComplaintSuccess() {
// Arrange
Long complaintId = 1L;

// Act
complaintService.deleteComplaint(complaintId);

// Assert
verify(complaintRepository, times(1)).deleteById(complaintId);
}

@Test
public void testGetComplaintNotFound() {
// Arrange
Long complaintId = 1L;
when(complaintRepository.findById(complaintId)).thenReturn(Optional.empty());

// Act
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> complaintService.getComplaint(complaintId));

// Assert
assertEquals("Complaint not found", exception.getMessage());
}

@Test
public void testGetComplaintSuccess() {
// Arrange
Long complaintId = 1L;
Complaint complaint = new Complaint();
ComplaintResponseDto complaintResponseDto = new ComplaintResponseDto();

when(complaintRepository.findById(complaintId)).thenReturn(Optional.of(complaint));
when(complaintMapper.mapComplaintToDto(complaint)).thenReturn(complaintResponseDto);

// Act
ComplaintResponseDto result = complaintService.getComplaint(complaintId);

// Assert
assertEquals(complaintResponseDto, result);
}

@Test
public void testGetComplaintsSuccess() {
// Arrange
ComplaintStatus status = ComplaintStatus.PENDING;
int page = 1;
int size = 10;
Sort.Direction direction = Sort.Direction.ASC;

Page<Complaint> complaintPage = new PageImpl<>(Collections.emptyList());
List<Complaint> complaintList = complaintPage.getContent();
List<ComplaintResponseDto> complaintResponseDtoList = Collections.emptyList();

when(complaintRepository.findAllByStatus(status,
PageRequest.of(0, size, Sort.by(direction, "created")))).thenReturn(complaintPage);
when(complaintMapper.mapComplaintToDto(complaintList)).thenReturn(complaintResponseDtoList);

// Act
List<ComplaintResponseDto> result = complaintService.getComplaints(status, size, page, direction);

// Assert
assertEquals(complaintResponseDtoList, result);
}

@Test
public void testGetComplaintsByUserIdSuccess() {
// Arrange
Long userId = 1L;
ComplaintStatus status = ComplaintStatus.PENDING;
int page = 1;
int size = 10;
Sort.Direction direction = Sort.Direction.ASC;

Page<Complaint> complaintPage = new PageImpl<>(Collections.emptyList());
List<Complaint> complaintList = complaintPage.getContent();
List<ComplaintResponseDto> complaintResponseDtoList = Collections.emptyList();

when(complaintRepository.findAllByComplainedUserIdAndStatus(userId, status,
PageRequest.of(0, size, Sort.by(direction, "created"))))
.thenReturn(complaintPage);
when(complaintMapper.mapComplaintToDto(complaintList)).thenReturn(complaintResponseDtoList);

// Act
List<ComplaintResponseDto> result = complaintService
.getComplaintsByUserId(userId, status, size, page, direction);

// Assert
assertEquals(complaintResponseDtoList, result);
}

@Test
public void testUpdateStatusById_Success() {
// Arrange
Long complaintId = 1L;
ComplaintStatus status = ComplaintStatus.RESOLVED;

// Act
complaintService.updateStatusById(complaintId, status);

// Assert
verify(complaintRepository, times(1)).updateStatusById(complaintId, status);
}
}