Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
<dependency>
<groupId>com.peralta.cashflow</groupId>
<artifactId>cashflow-commons</artifactId>
<version>0.0.2</version>
<version>0.0.3</version>
</dependency>
<dependency>
<groupId>com.peralta.cashflow</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package com.cashflow.coredata.controller.category;

import com.cashflow.commons.core.dto.request.BaseRequest;
import com.cashflow.commons.core.dto.request.PageRequest;
import com.cashflow.coredata.domain.dto.request.category.CategoryCreationRequest;
import com.cashflow.coredata.domain.dto.response.CategoryResponse;
import com.cashflow.coredata.service.category.ICategoryService;
import com.cashflow.exception.core.CashFlowException;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.Locale;

Expand All @@ -38,4 +37,13 @@ public CategoryResponse registerCategory(CategoryCreationRequest request, Locale
return categoryService.registerCategory(new BaseRequest<>(language, request));
}

@Override
@GetMapping("/list")
public Page<CategoryResponse> listCategories(Locale language, int pageNumber, int pageSize, String search) {
log.info("Received request to list categories..");
return categoryService.listCategories(new PageRequest<>(
pageNumber, pageSize, language, search
));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Locale;

Expand Down Expand Up @@ -50,4 +52,30 @@ CategoryResponse registerCategory(
@Valid @RequestBody CategoryCreationRequest request,
@RequestHeader(name = "Accept-Language", required = false, defaultValue = "en") Locale language
) throws CashFlowException;

@Operation(
summary = "List categories",
description = "Should return a paged response of categories from the provided request data",
security = @SecurityRequirement(name = "Authorization"),
parameters = {
@Parameter(name = "Accept-Language", description = "Language to be used on response messages", in = ParameterIn.HEADER, example = "en"),
@Parameter(name = "Authorization", description = "JWT token", in = ParameterIn.HEADER, required = true, example = "JWT.TOKEN.HERE"),
@Parameter(name = "pageNumber", description = "The requested page", in = ParameterIn.QUERY, example = "1"),
@Parameter(name = "pageSize", description = "The page size", in = ParameterIn.QUERY, example = "10"),
@Parameter(name = "search", description = "The filter text search", in = ParameterIn.QUERY, example = "filter")
}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "401", description = "Unauthorized - JWT token is missing or invalid",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))
),
@ApiResponse(responseCode = "200", description = "List of categories retrieved",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = CategoryResponse.class)))
})
Page<CategoryResponse> listCategories(
@RequestHeader(name = "Accept-Language", required = false, defaultValue = "en") Locale language,
@RequestParam(name = "pageNumber", required = false, defaultValue = "0") int pageNumber,
@RequestParam(name = "pageSize", required = false, defaultValue = "10") int pageSize,
@RequestParam(name = "search", required = false, defaultValue = "") String search
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.cashflow.coredata.repository.category;

import com.cashflow.coredata.domain.dto.response.CategoryResponse;
import com.cashflow.coredata.domain.entities.Category;
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.Query;
import org.springframework.stereotype.Repository;
Expand All @@ -15,4 +18,15 @@ public interface CategoryRepository extends JpaRepository<Category, Long> {
"AND category.active = true)", nativeQuery = true)
Long existsByNameIgnoreCase(String name, Long userId);

@Query(value = "SELECT new com.cashflow.coredata.domain.dto.response.CategoryResponse(" +
" category.id, " +
" category.name, " +
" category.color, " +
" category.icon " +
") FROM Category category " +
"WHERE category.userId = :userId " +
"AND UPPER(category.name) LIKE UPPER(CONCAT('%', :name, '%')) " +
"ORDER BY category.name ASC")
Page<CategoryResponse> findByNameLikeIgnoreCase(String name, Long userId, Pageable pageable);

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.cashflow.auth.core.utils.AuthUtils;
import com.cashflow.commons.core.dto.request.BaseRequest;
import com.cashflow.commons.core.dto.request.PageRequest;
import com.cashflow.coredata.domain.dto.request.category.CategoryCreationRequest;
import com.cashflow.coredata.domain.dto.response.CategoryResponse;
import com.cashflow.coredata.domain.entities.Category;
Expand All @@ -12,6 +13,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;

@Service
Expand Down Expand Up @@ -56,4 +58,19 @@ private boolean categoryExistsByName(String name, Long userId) {
return categoryRepository.existsByNameIgnoreCase(name, userId) == 1;
}

@Override
public Page<CategoryResponse> listCategories(PageRequest<Void> request) {

Long userId = AuthUtils.getUserIdFromSecurityContext();
String search = request.getSearch();

log.info("Searching user: {} categories with search: {}", userId, search);

Page<CategoryResponse> response = categoryRepository.findByNameLikeIgnoreCase(search, userId, request.getPageable());

log.info("Found {} categories!", response.getTotalElements());

return response;
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.cashflow.coredata.service.category;

import com.cashflow.commons.core.dto.request.BaseRequest;
import com.cashflow.commons.core.dto.request.PageRequest;
import com.cashflow.coredata.domain.dto.request.category.CategoryCreationRequest;
import com.cashflow.coredata.domain.dto.response.CategoryResponse;
import com.cashflow.exception.core.CashFlowException;
import org.springframework.data.domain.Page;

public interface ICategoryService {

CategoryResponse registerCategory(BaseRequest<CategoryCreationRequest> baseRequest) throws CashFlowException;
Page<CategoryResponse> listCategories(PageRequest<Void> request);

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.cashflow.coredata.controller.category;

import com.cashflow.coredata.domain.dto.response.CategoryResponse;
import com.cashflow.coredata.service.category.ICategoryService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
Expand All @@ -14,6 +17,9 @@
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import templates.category.CategoryTemplates;

import java.util.ArrayList;
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

Expand All @@ -29,6 +35,7 @@ class CategoryControllerTest {
private ICategoryService categoryService;

private static final String BASE_REQUEST_URL = "/core/category";
private final CategoryResponse categoryResponse = CategoryTemplates.categoryResponse();

@Autowired
CategoryControllerTest(final MockMvc mockMvc) {
Expand All @@ -42,13 +49,25 @@ void givenCategoryCreationRequest_whenRegisterCategory_thenCategoryResponseIsRet

String jsonRequest = objectMapper.writeValueAsString(CategoryTemplates.categoryCreationRequest());

var response = CategoryTemplates.categoryResponse();
when(categoryService.registerCategory(any())).thenReturn(response);
when(categoryService.registerCategory(any())).thenReturn(categoryResponse);

mockMvc.perform(MockMvcRequestBuilders.post(BASE_REQUEST_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(jsonRequest))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().json(objectMapper.writeValueAsString(response)));
.andExpect(MockMvcResultMatchers.content().json(objectMapper.writeValueAsString(categoryResponse)));
}

@Test
@SneakyThrows
void givenParameter_whenListCategories_thenReturnCategoryResponsePage() {

Page<CategoryResponse> response = new PageImpl<>(new ArrayList<>(List.of(categoryResponse)));

when(categoryService.listCategories(any())).thenReturn(response);

mockMvc.perform(MockMvcRequestBuilders.get(BASE_REQUEST_URL + "/list")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.cashflow.coredata.repository.category;

import com.cashflow.commons.core.dto.request.PageRequest;
import com.cashflow.coredata.domain.dto.response.CategoryResponse;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.test.context.ActiveProfiles;

import java.util.Locale;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@ActiveProfiles("test")
class CategoryRepositoryTest {

@Autowired
private CategoryRepository categoryRepository;

private final PageRequest<Void> pageRequest = new PageRequest<>(0, 2, Locale.ENGLISH, "");

@Test
void givenFoodFilter_whenFindByNameLikeIgnoreCase_thenReturnOneResult() {
Page<CategoryResponse> response = categoryRepository.findByNameLikeIgnoreCase("food", 5L, pageRequest.getPageable());
assertAll(() -> {
assertEquals(1, response.getTotalElements());
assertEquals("Food", response.getContent().getFirst().name());
});
}

@Test
void givenUserIdWithoutCategories_whenFindByNameLikeIgnoreCase_thenReturnNoResult() {
Page<CategoryResponse> response = categoryRepository.findByNameLikeIgnoreCase("", 404L, pageRequest.getPageable());
assertEquals(0, response.getTotalElements());
}

@Test
void givenEmptyFilterSearch_whenFindByNameLikeIgnoreCase_thenReturnPagedResponse() {
Page<CategoryResponse> response = categoryRepository.findByNameLikeIgnoreCase("", 5L, pageRequest.getPageable());
assertAll(() -> {
assertEquals(3L, response.getTotalElements());
assertEquals(2, response.getTotalPages());
assertEquals(2, response.getContent().size());
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.cashflow.auth.core.domain.authentication.CashFlowAuthentication;
import com.cashflow.commons.core.dto.request.BaseRequest;
import com.cashflow.commons.core.dto.request.PageRequest;
import com.cashflow.coredata.domain.dto.request.category.CategoryCreationRequest;
import com.cashflow.coredata.domain.dto.response.CategoryResponse;
import com.cashflow.coredata.domain.entities.Category;
Expand All @@ -16,13 +17,16 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.MessageSource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ActiveProfiles;
import templates.category.CategoryTemplates;
import templates.security.AuthenticationTemplates;

import java.util.List;
import java.util.Locale;
import java.util.Objects;

Expand Down Expand Up @@ -54,6 +58,8 @@ class CategoryServiceTest {

private final CashFlowAuthentication authentication = AuthenticationTemplates.cashFlowAuthentication();

private final CategoryResponse categoryResponse = CategoryTemplates.categoryResponse();

@BeforeEach
void setup() {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Expand Down Expand Up @@ -104,4 +110,23 @@ void givenNewCategory_whenRegisterCategory_thenReturnCategoryResponse() {
});
}

@Test
void givenPageRequest_whenListCategories_thenReturnCategoryResponsePage() {

PageRequest<Void> pageRequest = new PageRequest<>(0, 10, locale, "search");
Page<CategoryResponse> pageResponse = new PageImpl<>(List.of(categoryResponse));

when(categoryRepository.findByNameLikeIgnoreCase(
"search",
Objects.requireNonNull(authentication.getCredentials()).id(),
pageRequest.getPageable()
)).thenReturn(pageResponse);

assertEquals(
pageResponse,
categoryService.listCategories(pageRequest)
);

}

}
20 changes: 20 additions & 0 deletions src/test/resources/data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Category table creation
CREATE TABLE tb_category (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
color VARCHAR(20) NULL,
icon VARCHAR(20),
active BOOLEAN NOT NULL,
user_id BIGINT NOT NULL
);

-- Generating category base
INSERT INTO tb_category
(name, color, icon, active, user_id)
VALUES('Food', '#c43030', 'πŸ—', 1, 5);
INSERT INTO tb_category
(name, color, icon, active, user_id)
VALUES('Miscelaneous', NULL, NULL, 1, 5);
INSERT INTO tb_category
(name, color, icon, active, user_id)
VALUES('Salary', '#12e80e', 'πŸ’΅', 1, 5);