Skip to content

Commit

Permalink
Expose Query Plans API
Browse files Browse the repository at this point in the history
  • Loading branch information
semotpan committed Aug 12, 2024
1 parent 56084b3 commit 6818dd4
Show file tree
Hide file tree
Showing 16 changed files with 543 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package io.myfinbox.spendingplan.adapter.web;

import io.myfinbox.rest.CreateJarResource;
import io.myfinbox.rest.JarCategoryModificationResource;
import io.myfinbox.rest.JarResource;
import io.myfinbox.shared.ApiFailureHandler;
import io.myfinbox.spendingplan.application.AddOrRemoveJarCategoryUseCase;
import io.myfinbox.spendingplan.application.AddOrRemoveJarCategoryUseCase.JarCategoryToAddOrRemove;
import io.myfinbox.spendingplan.application.CreateJarUseCase;
import io.myfinbox.spendingplan.application.JarCommand;
import io.myfinbox.spendingplan.domain.Jar;
import lombok.RequiredArgsConstructor;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

Expand All @@ -29,12 +29,13 @@ final class JarController implements JarsApi {
private final CreateJarUseCase createJarUseCase;
private final AddOrRemoveJarCategoryUseCase addOrRemoveJarCategoryUseCase;
private final ApiFailureHandler apiFailureHandler;
private final ConversionService conversionService;

@PostMapping(path = "/{planId}/jars", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> create(@PathVariable UUID planId, @RequestBody CreateJarResource resource) {
public ResponseEntity<?> create(@PathVariable UUID planId, @RequestBody JarResource resource) {
return createJarUseCase.create(planId, toCommand(resource))
.fold(apiFailureHandler::handle, jar -> created(fromCurrentRequest().path("/{id}").build(jar.getId().id()))
.body(toResource(jar)));
.body(conversionService.convert(jar, JarResource.class)));
}

@PutMapping(path = "/{planId}/jars/{jarId}/expense-categories", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
Expand All @@ -45,25 +46,14 @@ public ResponseEntity<?> modifyExpenseCategories(@PathVariable UUID planId,
.fold(apiFailureHandler::handle, ok -> ok().build());
}

private JarCommand toCommand(CreateJarResource resource) {
private JarCommand toCommand(JarResource resource) {
return JarCommand.builder()
.name(resource.getName())
.percentage(resource.getPercentage())
.description(resource.getDescription())
.build();
}

private CreateJarResource toResource(Jar jar) {
return new CreateJarResource()
.jarId(jar.getId().id())
.creationTimestamp(jar.getCreationTimestamp().toString())
.amountToReach(jar.getAmountToReachAsNumber())
.currencyCode(jar.getCurrencyCode())
.name(jar.getName())
.percentage(jar.getPercentage().value())
.description(jar.getDescription());
}

private JarCategoriesCommand toCommand(JarCategoryModificationResource resource) {
var categoryToAdds = resource.getCategories()
.stream()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.myfinbox.spendingplan.adapter.web;

import io.myfinbox.rest.CreateJarResource;
import io.myfinbox.rest.JarCategoryModificationResource;
import io.myfinbox.rest.JarResource;
import io.myfinbox.shared.ApiErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand Down Expand Up @@ -32,7 +32,7 @@ public interface JarsApi {
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Successful Operation",
headers = @Header(name = LOCATION, description = "Created jar source URI location", schema = @Schema(implementation = URI.class)),
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = CreateJarResource.class))),
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = JarResource.class))),
@ApiResponse(responseCode = "400", description = "Malformed JSON or Type Mismatch Failure",
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))),
@ApiResponse(responseCode = "404", description = "Spending plan not found",
Expand All @@ -45,7 +45,7 @@ public interface JarsApi {
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class)))
})
ResponseEntity<?> create(@Parameter(in = PATH, description = "Spending plan ID to be added new jar", required = true) UUID planId,
@RequestBody(description = "Jar Resource to be created", required = true) CreateJarResource resource);
@RequestBody(description = "Jar Resource to be created", required = true) JarResource resource);

@Operation(
summary = "Modify Expense Categories for Spending Plan Jar",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
import io.myfinbox.rest.CreateClassicPlanResource;
import io.myfinbox.rest.PlanResource;
import io.myfinbox.shared.ApiFailureHandler;
import io.myfinbox.spendingplan.application.ClassicPlanBuilderUseCase;
import io.myfinbox.shared.Failure;
import io.myfinbox.spendingplan.application.*;
import io.myfinbox.spendingplan.application.ClassicPlanBuilderUseCase.CreateClassicPlanCommand;
import io.myfinbox.spendingplan.application.CreatePlanUseCase;
import io.myfinbox.spendingplan.application.PlanCommand;
import io.myfinbox.spendingplan.application.UpdatePlanUseCase;
import io.myfinbox.spendingplan.domain.Plan;
import lombok.RequiredArgsConstructor;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

Expand All @@ -28,37 +26,52 @@ final class PlanController implements PlansApi {
private final CreatePlanUseCase createPlanUseCase;
private final ClassicPlanBuilderUseCase classicPlanBuilderUseCase;
private final UpdatePlanUseCase updatePlanUseCase;
private final PlanQuery planQuery;
private final ApiFailureHandler apiFailureHandler;
private final ConversionService conversionService;

@PostMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> create(@RequestBody PlanResource resource) {
return createPlanUseCase.create(toCommand(resource))
.fold(apiFailureHandler::handle, plan -> created(fromCurrentRequest().path("/{id}").build(plan.getId().id()))
.body(toResource(plan)));
.body(conversionService.convert(plan, PlanResource.class)));
}

@PostMapping(path = "/classic", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> createClassic(@RequestBody CreateClassicPlanResource resource) {
return classicPlanBuilderUseCase.create(toCommand(resource))
.fold(apiFailureHandler::handle, plan -> created(fromCurrentRequest().path("/{id}").build(plan.getId().id()))
.body(toResource(plan)));
.body(conversionService.convert(plan, PlanResource.class)));
}

@PutMapping(path = "/{planId}", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> update(@PathVariable UUID planId, @RequestBody PlanResource resource) {
return updatePlanUseCase.update(planId, toCommand(resource))
.fold(apiFailureHandler::handle, plan -> ok().body(toResource(plan)));
.fold(apiFailureHandler::handle, plan -> ok().body(conversionService.convert(plan, PlanResource.class)));
}

private PlanResource toResource(Plan plan) {
return new PlanResource()
.planId(plan.getId().id())
.name(plan.getName())
.creationTimestamp(plan.getCreationTimestamp().toString())
.amount(plan.getAmountAsNumber())
.currencyCode(plan.getCurrencyCode())
.accountId(plan.getAccount().id())
.description(plan.getDescription());
@GetMapping(path = "/{planId}", produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> one(@PathVariable UUID planId) {
var plans = planQuery.search()
.withPlanId(planId)
.list();

if (plans.isEmpty()) {
return apiFailureHandler.handle(Failure.ofNotFound("Plan with ID '%s' was not found.".formatted(planId)));
}

return ok().body(conversionService.convert(plans.getFirst(), PlanResource.class));
}

@GetMapping(produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> list(@RequestParam("accountId") UUID accountId) {
var plans = planQuery.search()
.withAccountId(accountId)
.list();

return ok().body(plans.stream()
.map(plan -> conversionService.convert(plan, PlanResource.class))
.toList());
}

private PlanCommand toCommand(PlanResource resource) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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 @@ -18,6 +19,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 @@ -84,4 +86,35 @@ public interface PlansApi {
})
ResponseEntity<?> update(@Parameter(in = PATH, description = "PlanId to be updated", required = true) UUID planId,
@RequestBody(description = "Spending Plan Resource to be updated", required = true) PlanResource resource);

@Operation(summary = "Query a list of spending plans for a specified account in the MyFinBox",
description = "Query a list of spending plans 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 = PlanResource.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 spending plans", required = true) UUID accountId);

@Operation(summary = "Query a spending plan for a specified plan ID in the MyFinBox",
description = "Query a spending plan for a specified plan ID 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 = PlanResource.class)))),
@ApiResponse(responseCode = "400", description = "Malformed JSON or Type Mismatch Failure",
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))),
@ApiResponse(responseCode = "404", description = "Plan not found 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<?> one(@Parameter(in = PATH, description = "Plan Id to be used for searching", required = true) UUID planId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.myfinbox.spendingplan.adapter.web.converters;

import io.myfinbox.rest.JarResource;
import io.myfinbox.spendingplan.domain.Jar;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

@Component
final class JarToResourceConverter implements Converter<Jar, JarResource> {

@Override
public JarResource convert(Jar jar) {
return new JarResource()
.jarId(jar.getId().id())
.creationTimestamp(jar.getCreationTimestamp().toString())
.amountToReach(jar.getAmountToReachAsNumber())
.currencyCode(jar.getCurrencyCode())
.name(jar.getName())
.percentage(jar.getPercentage().value())
.description(jar.getDescription());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.myfinbox.spendingplan.adapter.web.converters;

import io.myfinbox.rest.JarResource;
import io.myfinbox.rest.PlanResource;
import io.myfinbox.spendingplan.domain.Jar;
import io.myfinbox.spendingplan.domain.Plan;
import lombok.RequiredArgsConstructor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
final class PlanToResourceConverter implements Converter<Plan, PlanResource> {

private final Converter<Jar, JarResource> jarResourceConverter;

@Override
public PlanResource convert(Plan plan) {
return new PlanResource()
.planId(plan.getId().id())
.name(plan.getName())
.creationTimestamp(plan.getCreationTimestamp().toString())
.amount(plan.getAmountAsNumber())
.currencyCode(plan.getCurrencyCode())
.accountId(plan.getAccount().id())
.description(plan.getDescription())
.jars(plan.getJars().stream()
.map(jarResourceConverter::convert)
.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.myfinbox.spendingplan.application;

import io.myfinbox.spendingplan.domain.Plan;

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

/**
* Represents a query interface for searching and retrieving plans.
*/
public interface PlanQuery {

/**
* Initiates a search for plans.
*
* @return a builder to further customize the plan search.
*/
PlanQueryBuilder search();

/**
* Builder interface for constructing and executing a plan query.
*/
interface PlanQueryBuilder {

/**
* Filters the plans by the specified plan ID.
*
* @param planId the unique identifier of the plan.
* @return the updated query builder.
*/
PlanQueryBuilder withPlanId(UUID planId);

/**
* Filters the plans by the specified account ID.
*
* @param accountId the unique identifier of the account.
* @return the updated query builder.
*/
PlanQueryBuilder withAccountId(UUID accountId);

/**
* Executes the query and returns a list of plans matching the criteria.
*
* @return a list of plans matching the query criteria.
*/
List<Plan> list();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.myfinbox.spendingplan.application;

import io.myfinbox.spendingplan.domain.AccountIdentifier;
import io.myfinbox.spendingplan.domain.Plan;
import io.myfinbox.spendingplan.domain.Plans;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

import static io.myfinbox.spendingplan.domain.Plan.PlanIdentifier;
import static java.util.Collections.emptyList;
import static java.util.Objects.nonNull;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
class PlanQueryService implements PlanQuery {

private final Plans plans;

@Override
public PlanQueryBuilder search() {
return new DefaultPlanQueryBuilder(plans);
}

@RequiredArgsConstructor
private static class DefaultPlanQueryBuilder implements PlanQueryBuilder {

private final Plans plans;

private UUID planId;
private UUID accountId;

@Override
public PlanQueryBuilder withPlanId(UUID planId) {
this.planId = planId;
return this;
}

@Override
public PlanQueryBuilder withAccountId(UUID accountId) {
this.accountId = accountId;
return this;
}

@Override
public List<Plan> list() {
if (nonNull(planId) && nonNull(accountId)) {
return plans.findByIdAndAccountIdEagerJars(new PlanIdentifier(planId), new AccountIdentifier(accountId))
.map(List::of)
.orElse(emptyList());
}

if (nonNull(accountId)) {
return plans.findByAccountIdEagerJars(new AccountIdentifier(accountId));
}

if (nonNull(planId)) {
return plans.findByIdEagerJars(new PlanIdentifier(planId))
.map(List::of)
.orElse(emptyList());
}

return emptyList();
}
}
}
Loading

0 comments on commit 6818dd4

Please sign in to comment.