Skip to content

Commit

Permalink
Expose Expense Categories query API
Browse files Browse the repository at this point in the history
  • Loading branch information
semotpan committed Aug 7, 2024
1 parent c6d5666 commit 51db2fc
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.myfinbox.expense.adapter.web;

import io.myfinbox.expense.application.CategoryQuery;
import io.myfinbox.expense.application.CategoryService;
import io.myfinbox.expense.domain.Category;
import io.myfinbox.rest.ExpenseCategoryResource;
Expand All @@ -21,6 +22,7 @@
final class ExpenseCategoryController implements ExpensesCategoryApi {

private final CategoryService categoryService;
private final CategoryQuery categoryQuery;
private final ApiFailureHandler apiFailureHandler;

@PostMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
Expand All @@ -43,6 +45,13 @@ public ResponseEntity<?> delete(@PathVariable UUID categoryId) {
.fold(apiFailureHandler::handle, ok -> noContent().build());
}

@GetMapping
public ResponseEntity<?> list(@RequestParam(value = "accountId") UUID accountId) {
return ok(categoryQuery.search(accountId).stream()
.map(this::toResource)
.toList());
}

private ExpenseCategoryResource toResource(Category category) {
return new ExpenseCategoryResource()
.categoryId(category.getId().id())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
Expand All @@ -17,6 +18,7 @@
import java.util.UUID;

import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH;
import static io.swagger.v3.oas.annotations.enums.ParameterIn.QUERY;
import static org.springframework.http.HttpHeaders.LOCATION;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

Expand Down Expand Up @@ -81,4 +83,18 @@ ResponseEntity<?> update(@Parameter(in = PATH, description = "CategoryId to be u
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class)))
})
ResponseEntity<?> delete(@Parameter(in = PATH, description = "CategoryId to be deleted", required = true) UUID categoryId);

@Operation(summary = "Query a list of expense categories for a specified account in the MyFinBox",
description = "Query a list of expense categories for a specified account in the MyFinBox",
security = {@SecurityRequirement(name = "openId")},
tags = {TAG})
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successful Operation",
content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = ExpenseCategoryResource.class)))),
@ApiResponse(responseCode = "400", description = "Malformed JSON or Type Mismatch Failure",
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))),
@ApiResponse(responseCode = "500", description = "Internal Server Error",
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class)))
})
ResponseEntity<?> list(@Parameter(in = QUERY, description = "Account to be used for selecting categories", required = true) UUID accountId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.myfinbox.expense.application;

import io.myfinbox.expense.domain.Category;

import java.util.List;
import java.util.UUID;

/**
* Interface for querying category information.
*/
public interface CategoryQuery {

/**
* Searches for categories associated with a specific account.
*
* @param accountId the unique identifier of the account to search categories for
* @return a list of {@link Category} objects associated with the specified account
*/
List<Category> search(UUID accountId);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.myfinbox.expense.application;

import io.myfinbox.expense.domain.AccountIdentifier;
import io.myfinbox.expense.domain.Categories;
import io.myfinbox.expense.domain.Category;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;

import static java.util.Collections.emptyList;
import static java.util.Objects.isNull;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
class CategoryQueryService implements CategoryQuery {

private final Categories categories;

@Override
public List<Category> search(UUID accountId) {
if (isNull(accountId)) {
return emptyList();
}

return categories.findByAccount(new AccountIdentifier(accountId));
}
}
4 changes: 4 additions & 0 deletions server/src/test/groovy/io/myfinbox/expense/DataSamples.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,8 @@ class DataSamples {
static newValidExpenseCategoryResource(map = [:]) {
JsonOutput.toJson(EXPENSE_CATEGORY_RESOURCE + map) as String
}

static newValidExpenseCategoryResourceList() {
JsonOutput.toJson([EXPENSE_CATEGORY_RESOURCE, EXPENSE_CATEGORY_RESOURCE + [categoryId: categoryId2, name: 'Other']]) as String
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.core.io.ClassPathResource
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.ResponseEntity
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.TestPropertySource
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.jdbc.JdbcTestUtils
import org.springframework.web.util.UriComponentsBuilder
import spock.lang.Specification
import spock.lang.Tag

import static io.myfinbox.expense.DataSamples.newValidExpenseCategoryResource
import static io.myfinbox.expense.DataSamples.newValidExpenseCategoryResourceList
import static org.skyscreamer.jsonassert.JSONCompareMode.LENIENT
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import static org.springframework.http.HttpMethod.DELETE
Expand Down Expand Up @@ -123,6 +126,18 @@ class ExpenseCategoryControllerSpec extends Specification {
JSONAssert.assertEquals(expectDeleteConflictFailure(), response.getBody(), LENIENT)
}

@Sql('/expense/web/expensecategory-create.sql')
def "should get a list with two expense category"() {
when: 'list expense category'
def response = getExpenses(UUID.fromString(DataSamples.accountId))

then: 'response status is OK'
assert response.getStatusCode() == OK

and: 'a list of two expense is present'
JSONAssert.assertEquals(newValidExpenseCategoryResourceList(), response.getBody(), LENIENT)
}

private postExpenseCategory(String request) {
restTemplate.postForEntity('/v1/expenses/category', entityRequest(request), String.class)
}
Expand All @@ -145,6 +160,20 @@ class ExpenseCategoryControllerSpec extends Specification {
)
}

def getExpenses(UUID accountId) {
def uri = UriComponentsBuilder.fromUriString("${restTemplate.getRootUri()}/v1/expenses/category")
.queryParam("accountId", accountId)
.build()
.toUri()

restTemplate.exchange(
uri,
HttpMethod.GET,
null,
String.class
)
}

def entityRequest(String req) {
var headers = new HttpHeaders()
headers.setContentType(APPLICATION_JSON)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.myfinbox.expense.application

import io.myfinbox.expense.domain.AccountIdentifier
import io.myfinbox.expense.domain.Categories
import spock.lang.Specification
import spock.lang.Tag

import static io.myfinbox.expense.DataSamples.newSampleCategory

@Tag("unit")
class CategoryQueryServiceSpec extends Specification {

Categories categories
CategoryQueryService service

def setup() {
categories = Mock()
service = new CategoryQueryService(categories)
}

def "should get empty list when categories for provided account id not found"() {
setup: 'mock the repository to return an empty list for any account identifier'
1 * categories.findByAccount(_ as AccountIdentifier) >> []

when: 'searching for categories with a non-exiting account ID'
def categoryList = service.search(UUID.randomUUID())

then: 'the result should be an empty list'
assert categoryList.isEmpty()
}

def "should get empty list when account id is null"() {
when: 'searching for categories with a null account ID'
def categoryList = service.search(null)

then: 'the result should be an empty list'
assert categoryList.isEmpty()

and: 'the repository should not be queried'
0 * categories.findByAccount(_ as AccountIdentifier)
}

def "should get a list of categories"() {
setup: 'mock the repository to return a list with a sample category for any account identifier'
1 * categories.findByAccount(_ as AccountIdentifier) >> [newSampleCategory()]

when: 'Searching for categories with a random account ID'
def categoryList = service.search(UUID.randomUUID())

then: 'the result should be a list with one category'
assert categoryList.size() == 1
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
INSERT INTO server.expense_category (id,
account_id,
creation_timestamp,
name)
VALUES ('3b257779-a5db-4e87-9365-72c6f8d4977d',
'e2709aa2-7907-4f78-98b6-0f36a0c1b5ca',
'2024-03-23T10:00:04.224870Z',
'Bills'),
('e2709aa2-7907-4f78-98b6-0f36a0c1b5ca',
'e2709aa2-7907-4f78-98b6-0f36a0c1b5ca',
'2024-03-23T10:00:04.224870Z',
'Other');

0 comments on commit 51db2fc

Please sign in to comment.