diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 41372ec..7c20938 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -21,7 +21,7 @@ jobs: contents: read strategy: matrix: - service: [retail-data-services, cart-service] + service: [product-api, cart-service] steps: - name: Checkout @@ -56,7 +56,7 @@ jobs: packages: write strategy: matrix: - service: [retail-data-services, cart-service] + service: [product-api, cart-service] steps: - name: Checkout diff --git a/README.md b/README.md index 3b6818c..56ce565 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ A collection of containerized microservices used for technical interviews. Candi ## Services -| Service | Port | Description | -| -------------------- | ---- | ----------------------------------------------------------------------------------------------------------------- | -| retail-data-services | 8080 | Read-only REST API serving synthetic product catalog, pricing, and inventory availability | -| cart-service | 8081 | Shopping cart REST API with CRUD operations. Calls retail-data-services at runtime for item and price enrichment. | +| Service | Port | Description | +| ------------ | ---- | -------------------------------------------------------------------------------------------------------- | +| product-api | 8080 | Read-only REST API serving synthetic product catalog, pricing, and inventory availability | +| cart-service | 8081 | Shopping cart REST API with CRUD operations. Calls product-api at runtime for item and price enrichment. | **Note:** All data returned by these services is mocked/sample data intended for interviewing purposes only. It does not represent real or production retail data. @@ -27,8 +27,8 @@ docker compose up The services will be available at: -- retail-data-services: -- cart-service: +- product-api: +- cart-service: To stop the services: @@ -38,148 +38,148 @@ docker compose down ### Option 2: docker run (individual services) -Build and run each service separately. Note that cart-service depends on retail-data-services, so retail-data-services must be running first. +Build and run each service separately. Note that cart-service depends on product-api, so product-api must be running first. ```sh # Build both JARs ./gradlew clean build -# Build and run retail-data-services -docker build -t retail-data-services retail-data-services/ -docker run -d -p 8080:8080 --name data retail-data-services +# Build and run product-api +docker build -t product-api product-api/ +docker run -d -p 8080:8080 --name product-api product-api # Build and run cart-service docker build -t cart-service cart-service/ -docker run -p 8081:8081 --name cart --link data:data cart-service +docker run -p 8081:8081 --name cart --link product-api:product-api cart-service ``` ### OpenAPI specs Both services expose Swagger UI and OpenAPI docs: -| Service | Swagger UI | API docs | -| -------------------- | --------------------------------------------------------------------- | -------------------------------------------------------- | -| retail-data-services | | | -| cart-service | | | +| Service | Swagger UI | API docs | +| ------------ | --------------------------------------------- | -------------------------------- | +| product-api | | | +| cart-service | | | HTTP request files for use with IntelliJ or VS Code are available at: -- `retail-data-services/retail-data-services.http` +- `product-api/product-api.http` - `cart-service/cart-service.http` -## retail-data-services endpoints +## product-api endpoints ### Get price -**`GET /retail_data_services/v1/prices/{id}`** +**`GET /v1/prices/{id}`** ```sh -curl -X GET "http://localhost:8080/retail_data_services/v1/prices/123456" +curl -X GET "http://localhost:8080/v1/prices/123456" ``` ### Get item -**`GET /retail_data_services/v1/items/{id}`** +**`GET /v1/items/{id}`** ```sh -curl -X GET "http://localhost:8080/retail_data_services/v1/items/123456" +curl -X GET "http://localhost:8080/v1/items/123456" ``` ### List items -**`GET /retail_data_services/v1/items`** +**`GET /v1/items`** Supports filtering by `small_description` query parameter. ```sh -curl -X GET "http://localhost:8080/retail_data_services/v1/items" -curl -X GET "http://localhost:8080/retail_data_services/v1/items?small_description=jersey" +curl -X GET "http://localhost:8080/v1/items" +curl -X GET "http://localhost:8080/v1/items?small_description=jersey" ``` ### Get availability -**`GET /retail_data_services/v1/availability/{id}`** +**`GET /v1/availability/{id}`** ```sh -curl -X GET "http://localhost:8080/retail_data_services/v1/availability/123456" +curl -X GET "http://localhost:8080/v1/availability/123456" ``` ## cart-service endpoints -cart-service depends on retail-data-services at runtime. When a cart is read, the service calls retail-data-services over HTTP to enrich each line item with product details and pricing. It then calculates taxes (by product category) and delivery charges. +cart-service depends on product-api at runtime. When a cart is read, the service calls product-api over HTTP to enrich each line item with product details and pricing. It then calculates taxes (by product category) and delivery charges. ### Get cart -**`GET /cart/v1/carts/{id}`** +**`GET /v1/carts/{id}`** ```sh -curl 'http://localhost:8081/cart/v1/carts/100' -i -X GET +curl 'http://localhost:8081/v1/carts/100' -i -X GET ``` ### Create cart -**`POST /cart/v1/carts`** +**`POST /v1/carts`** -Request body: array of objects with `tcin` (string) and `quantity` (integer). +Request body: array of objects with `item_id` (string) and `quantity` (integer). ```sh -curl 'http://localhost:8081/cart/v1/carts' -i -X POST \ +curl 'http://localhost:8081/v1/carts' -i -X POST \ -H 'Content-Type: application/json' \ -d '[ - {"tcin" : "123456", "quantity": 1}, - {"tcin" : "789123", "quantity": 2} + {"item_id" : "123456", "quantity": 1}, + {"item_id" : "789123", "quantity": 2} ]' ``` ### Add item to cart -**`POST /cart/v1/carts/{id}/items`** +**`POST /v1/carts/{id}/items`** ```sh -curl 'http://localhost:8081/cart/v1/carts/100/items' -i -X POST \ +curl 'http://localhost:8081/v1/carts/100/items' -i -X POST \ -H 'Content-Type: application/json' \ - -d '{"tcin" : "456788", "quantity": 2}' + -d '{"item_id" : "456788", "quantity": 2}' ``` ### Update item quantity -**`PATCH /cart/v1/carts/{id}/items/{tcin}`** +**`PATCH /v1/carts/{id}/items/{item_id}`** ```sh -curl 'http://localhost:8081/cart/v1/carts/100/items/456788' -i -X PATCH \ +curl 'http://localhost:8081/v1/carts/100/items/456788' -i -X PATCH \ -H 'Content-Type: application/json' \ -d '{"quantity": 3}' ``` ### Remove item from cart -**`DELETE /cart/v1/carts/{id}/items/{tcin}`** +**`DELETE /v1/carts/{id}/items/{item_id}`** Removing the last item from a cart also removes the cart. ```sh -curl 'http://localhost:8081/cart/v1/carts/100/items/456788' -i -X DELETE +curl 'http://localhost:8081/v1/carts/100/items/456788' -i -X DELETE ``` ## Customizing data -You can customize the data returned by retail-data-services by creating your own CSV files and mounting them into the container. See [retail-data-services/data-formats.md](retail-data-services/data-formats.md) for details. +You can customize the data returned by product-api by creating your own CSV files and mounting them into the container. See [product-api/data-formats.md](product-api/data-formats.md) for details. ## Induced behaviors (latency and failure simulation) Both services support configurable induced behaviors that simulate latency and failures. By setting the `DEFAULT_BEHAVIOR` environment variable, you can run the same APIs in different modes (normal, slow, or randomly failing) without changing any code. -See [retail-data-services/induced_behaviors.md](retail-data-services/induced_behaviors.md) for available modes, environment variables, and usage examples. +See [product-api/induced_behaviors.md](product-api/induced_behaviors.md) for available modes, environment variables, and usage examples. ## Performance benchmarking -A startup time benchmarking script is available at `retail-data-services/scripts/benchmark-startup.sh`. See `retail-data-services/scripts/README.md` for usage details. +A startup time benchmarking script is available at `product-api/scripts/benchmark-startup.sh`. See `product-api/scripts/README.md` for usage details. ## Project structure ```txt tech-case-studies/ - retail-data-services/ # Read-only data API (port 8080) + product-api/ # Read-only data API (port 8080) src/ Dockerfile build.gradle.kts @@ -192,3 +192,4 @@ tech-case-studies/ settings.gradle.kts # Multi-project includes gradle/libs.versions.toml # Shared dependency versions ``` + diff --git a/cart-service/src/main/java/com/target/retail/cart/controller/CartController.java b/cart-service/src/main/java/com/target/retail/cart/controller/CartController.java index 61db348..9e4eb89 100644 --- a/cart-service/src/main/java/com/target/retail/cart/controller/CartController.java +++ b/cart-service/src/main/java/com/target/retail/cart/controller/CartController.java @@ -4,7 +4,6 @@ import com.target.retail.cart.controller.dto.CartResponse; import com.target.retail.cart.controller.dto.UpdateItemRequest; import com.target.retail.cart.model.Cart; -import com.target.retail.cart.model.CartLineItem; import com.target.retail.cart.service.CartService; import com.target.retail.cart.service.behavior.Behaviors; import io.swagger.v3.oas.annotations.Operation; @@ -21,7 +20,7 @@ import java.util.stream.Collectors; @RestController -@RequestMapping("/cart/v1") +@RequestMapping("/v1") public class CartController { private CartService cartService; @@ -39,18 +38,18 @@ public CartController(CartService cartService, Behaviors behaviors) { @ApiResponse(responseCode = "200", description = "Successfully created cart", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = CartResponse.class))}), - @ApiResponse(responseCode = "400", description = "Invalid request due to duplicate TCINs", + @ApiResponse(responseCode = "400", description = "Invalid request due to duplicate item IDs", content = @Content) }) @PostMapping("/carts") public ResponseEntity createCart(@RequestBody List addItems) { - if (addItems.stream().map(AddItemRequest::tcin).distinct().count() != addItems.size()) { + if (addItems.stream().map(AddItemRequest::itemId).distinct().count() != addItems.size()) { return ResponseEntity.badRequest().build(); } Map itemsInCart = addItems.stream() - .collect(Collectors.toMap(AddItemRequest::tcin, AddItemRequest::quantity)); + .collect(Collectors.toMap(AddItemRequest::itemId, AddItemRequest::quantity)); String cartId = cartService.createCart(itemsInCart); @@ -83,12 +82,9 @@ public ResponseEntity getCart(@PathVariable String id) { @ApiResponse(responseCode = "404", description = "Cart not found", content = @Content) }) - @DeleteMapping("/carts/{id}/items/{tcin}") - public ResponseEntity removeItemFromCart(@PathVariable String id, @PathVariable String tcin) { - if(cartService.getCart(id).isEmpty()) { - return ResponseEntity.notFound().build(); - } - cartService.removeItem(id, tcin); + @DeleteMapping("/carts/{id}/items/{itemId}") + public ResponseEntity removeItemFromCart(@PathVariable String id, @PathVariable String itemId) { + cartService.removeItem(id, itemId); if (cartService.getCart(id).isEmpty()) { return ResponseEntity.noContent().build(); } else { @@ -106,10 +102,7 @@ public ResponseEntity removeItemFromCart(@PathVariable String id, }) @PostMapping("/carts/{id}/items") public ResponseEntity addItem(@PathVariable String id, @RequestBody AddItemRequest addItemRequest) { - if(cartService.getCart(id).isEmpty()) { - return ResponseEntity.notFound().build(); - } - cartService.addItem(id, addItemRequest.tcin(), addItemRequest.quantity()); + cartService.addItem(id, addItemRequest.itemId(), addItemRequest.quantity()); return getCart(id); } @@ -121,18 +114,10 @@ public ResponseEntity addItem(@PathVariable String id, @RequestBod @ApiResponse(responseCode = "404", description = "Cart or item not found", content = @Content) }) - @PatchMapping("/carts/{id}/items/{tcin}") - public ResponseEntity updateItem(@PathVariable String id, @PathVariable String tcin, @RequestBody UpdateItemRequest updateItemRequest) { - - Optional cartLineItem = cartService.getCart(id) - .flatMap( it -> it.findByTcin(tcin)); - if(cartLineItem.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - cartService.updateCartItem(id, tcin, updateItemRequest.quantity()); + @PatchMapping("/carts/{id}/items/{itemId}") + public ResponseEntity updateItem(@PathVariable String id, @PathVariable String itemId, @RequestBody UpdateItemRequest updateItemRequest) { + cartService.updateCartItem(id, itemId, updateItemRequest.quantity()); return getCart(id); - } } diff --git a/cart-service/src/main/java/com/target/retail/cart/controller/GlobalExceptionHandler.java b/cart-service/src/main/java/com/target/retail/cart/controller/GlobalExceptionHandler.java new file mode 100644 index 0000000..6eb2923 --- /dev/null +++ b/cart-service/src/main/java/com/target/retail/cart/controller/GlobalExceptionHandler.java @@ -0,0 +1,39 @@ +package com.target.retail.cart.controller; + +import com.target.retail.cart.controller.dto.ErrorResponse; +import com.target.retail.cart.data.DataException; +import com.target.retail.cart.exception.CartLineItemNotFoundException; +import com.target.retail.cart.exception.CartNotFoundException; +import com.target.retail.cart.exception.InducedFailureException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler({CartNotFoundException.class, CartLineItemNotFoundException.class}) + public ResponseEntity handleNotFound(RuntimeException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage())); + } + + @ExceptionHandler(InducedFailureException.class) + public ResponseEntity handleInducedFailure(InducedFailureException ex) { + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) + .body(new ErrorResponse(HttpStatus.SERVICE_UNAVAILABLE.value(), ex.getMessage())); + } + + @ExceptionHandler(DataException.class) + public ResponseEntity handleDataException(DataException ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An internal data error occurred")); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred")); + } +} diff --git a/cart-service/src/main/java/com/target/retail/cart/controller/dto/AddItemRequest.java b/cart-service/src/main/java/com/target/retail/cart/controller/dto/AddItemRequest.java index 86b7575..696afd8 100644 --- a/cart-service/src/main/java/com/target/retail/cart/controller/dto/AddItemRequest.java +++ b/cart-service/src/main/java/com/target/retail/cart/controller/dto/AddItemRequest.java @@ -1,4 +1,8 @@ package com.target.retail.cart.controller.dto; -public record AddItemRequest(String tcin, Integer quantity) { +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record AddItemRequest(String itemId, Integer quantity) { } diff --git a/cart-service/src/main/java/com/target/retail/cart/controller/dto/CartResponse.java b/cart-service/src/main/java/com/target/retail/cart/controller/dto/CartResponse.java index d765adf..d3fbb6c 100644 --- a/cart-service/src/main/java/com/target/retail/cart/controller/dto/CartResponse.java +++ b/cart-service/src/main/java/com/target/retail/cart/controller/dto/CartResponse.java @@ -41,7 +41,7 @@ private String formatNumber(BigDecimal bigDecimal) { } - public record ItemResponse(String tcin, String title, String description, String brand, String merchClass, Integer quantity, PriceResponse price, ImageResponse imageData) { + public record ItemResponse(String itemId, String title, String description, String brand, Integer merchClass, Integer quantity, PriceResponse price, ImageResponse imageData) { public record PriceResponse(BigDecimal regular, BigDecimal sale) {} @@ -50,9 +50,9 @@ public record ImageResponse(String primary, String alternate, String baseUrl) {} public static CartResponse from(Cart cart) { - List items = cart.cartLineItems().stream().map(it -> new ItemResponse(it.item().tcin(), + List items = cart.cartLineItems().stream().map(it -> new ItemResponse(it.item().itemId(), it.item().title(), it.item().description(), it.item().brand(), it.item().merchClass(), - it.quantity(), new ItemResponse.PriceResponse(it.price().regularPrice(), it.price().salePrice().orElse(null)), new ItemResponse.ImageResponse(it.item().primaryImage(), it.item().alternateImage(), it.item().baseUrl()))).toList(); + it.quantity(), new ItemResponse.PriceResponse(it.price().regular(), it.price().sale().orElse(null)), new ItemResponse.ImageResponse(it.item().primary(), it.item().alternate(), it.item().baseUrl()))).toList(); return new CartResponse(cart.id(), items, cart.subTotal(), cart.deliveryCharges(), cart.totalTax(), cart.getTotal(), cart.createdOn(), cart.updatedOn()); } diff --git a/cart-service/src/main/java/com/target/retail/cart/controller/dto/ErrorResponse.java b/cart-service/src/main/java/com/target/retail/cart/controller/dto/ErrorResponse.java new file mode 100644 index 0000000..70f177c --- /dev/null +++ b/cart-service/src/main/java/com/target/retail/cart/controller/dto/ErrorResponse.java @@ -0,0 +1,4 @@ +package com.target.retail.cart.controller.dto; + +public record ErrorResponse(int status, String message) { +} diff --git a/cart-service/src/main/java/com/target/retail/cart/exception/CartLineItemNotFoundException.java b/cart-service/src/main/java/com/target/retail/cart/exception/CartLineItemNotFoundException.java new file mode 100644 index 0000000..e7e9ca9 --- /dev/null +++ b/cart-service/src/main/java/com/target/retail/cart/exception/CartLineItemNotFoundException.java @@ -0,0 +1,7 @@ +package com.target.retail.cart.exception; + +public class CartLineItemNotFoundException extends RuntimeException { + public CartLineItemNotFoundException(String itemId) { + super("No cart line found for item id " + itemId); + } +} diff --git a/cart-service/src/main/java/com/target/retail/cart/exception/CartNotFoundException.java b/cart-service/src/main/java/com/target/retail/cart/exception/CartNotFoundException.java new file mode 100644 index 0000000..a08c805 --- /dev/null +++ b/cart-service/src/main/java/com/target/retail/cart/exception/CartNotFoundException.java @@ -0,0 +1,7 @@ +package com.target.retail.cart.exception; + +public class CartNotFoundException extends RuntimeException { + public CartNotFoundException(String cartId) { + super("No cart found with id " + cartId); + } +} diff --git a/cart-service/src/main/java/com/target/retail/cart/exception/InducedFailureException.java b/cart-service/src/main/java/com/target/retail/cart/exception/InducedFailureException.java new file mode 100644 index 0000000..2b0e1ec --- /dev/null +++ b/cart-service/src/main/java/com/target/retail/cart/exception/InducedFailureException.java @@ -0,0 +1,7 @@ +package com.target.retail.cart.exception; + +public class InducedFailureException extends RuntimeException { + public InducedFailureException(String message) { + super(message); + } +} diff --git a/cart-service/src/main/java/com/target/retail/cart/model/Cart.java b/cart-service/src/main/java/com/target/retail/cart/model/Cart.java index 71919ea..7c86f0e 100644 --- a/cart-service/src/main/java/com/target/retail/cart/model/Cart.java +++ b/cart-service/src/main/java/com/target/retail/cart/model/Cart.java @@ -24,7 +24,7 @@ public ZonedDateTime updatedOn() { return cartLineItems().stream().map(CartLineItem::updatedOn).max(ZonedDateTime::compareTo).orElse(ZonedDateTime.now()); } - public Optional findByTcin(String tcin) { - return cartLineItems.stream().filter(it -> it.item().tcin().equals(tcin)).findFirst(); + public Optional findByItemId(String itemId) { + return cartLineItems.stream().filter(it -> it.item().itemId().equals(itemId)).findFirst(); } } diff --git a/cart-service/src/main/java/com/target/retail/cart/model/Item.java b/cart-service/src/main/java/com/target/retail/cart/model/Item.java index 4ab7fab..47cfb58 100644 --- a/cart-service/src/main/java/com/target/retail/cart/model/Item.java +++ b/cart-service/src/main/java/com/target/retail/cart/model/Item.java @@ -1,14 +1,14 @@ package com.target.retail.cart.model; public record Item( - String tcin, + String itemId, String title, String description, String brand, String category, - String merchClass, - String primaryImage, - String alternateImage, + Integer merchClass, + String primary, + String alternate, String baseUrl ) { } diff --git a/cart-service/src/main/java/com/target/retail/cart/model/Price.java b/cart-service/src/main/java/com/target/retail/cart/model/Price.java index c7ede55..02f12e0 100644 --- a/cart-service/src/main/java/com/target/retail/cart/model/Price.java +++ b/cart-service/src/main/java/com/target/retail/cart/model/Price.java @@ -3,9 +3,9 @@ import java.math.BigDecimal; import java.util.Optional; -public record Price(String tcin, BigDecimal regularPrice, Optional salePrice) { +public record Price(String itemId, BigDecimal regular, Optional sale) { public BigDecimal getCurrentPrice(){ - return salePrice.orElse(regularPrice); + return sale.orElse(regular); } } diff --git a/cart-service/src/main/java/com/target/retail/cart/model/StoredCartLine.java b/cart-service/src/main/java/com/target/retail/cart/model/StoredCartLine.java index cae0d73..8342e5d 100644 --- a/cart-service/src/main/java/com/target/retail/cart/model/StoredCartLine.java +++ b/cart-service/src/main/java/com/target/retail/cart/model/StoredCartLine.java @@ -9,9 +9,9 @@ import java.time.ZonedDateTime; @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -@JsonPropertyOrder({ "lineId", "cartId", "tcin", "quantity", "createdOn", "updatedOn" }) +@JsonPropertyOrder({ "lineId", "cartId", "itemId", "quantity", "createdOn", "updatedOn" }) @JsonIgnoreProperties(ignoreUnknown = true) -public record StoredCartLine(String lineId, String cartId, String tcin, Integer quantity, ZonedDateTime createdOn, ZonedDateTime updatedOn) implements Identifiable { +public record StoredCartLine(String lineId, String cartId, String itemId, Integer quantity, ZonedDateTime createdOn, ZonedDateTime updatedOn) implements Identifiable { public String getId() { return lineId(); } diff --git a/cart-service/src/main/java/com/target/retail/cart/service/CartService.java b/cart-service/src/main/java/com/target/retail/cart/service/CartService.java index b4c200d..83d8e23 100644 --- a/cart-service/src/main/java/com/target/retail/cart/service/CartService.java +++ b/cart-service/src/main/java/com/target/retail/cart/service/CartService.java @@ -1,6 +1,8 @@ package com.target.retail.cart.service; import com.target.retail.cart.data.CartDatabase; +import com.target.retail.cart.exception.CartLineItemNotFoundException; +import com.target.retail.cart.exception.CartNotFoundException; import com.target.retail.cart.service.client.dto.ItemApiResponse; import com.target.retail.cart.service.client.dto.PriceApiResponse; import com.target.retail.cart.model.StoredCartLine; @@ -82,21 +84,21 @@ public Optional getCart(String cartId) { } - public void removeItem(String cartId, String tcin) { + public void removeItem(String cartId, String itemId) { Optional cart = getCart(cartId); if (cart.isEmpty()) { - throw new RuntimeException("No cart found with id " + cartId); + throw new CartNotFoundException(cartId); } List updatedItems = cart.get().cartLineItems().stream() - .filter(it -> !it.item().tcin().equals(tcin)) + .filter(it -> !it.item().itemId().equals(itemId)) .collect(Collectors.toList()); List storedCartLines = updatedItems.stream() .map(it -> new StoredCartLine(it.lineItemId(), cart.get().id(), - it.item().tcin(), + it.item().itemId(), it.quantity(), it.createdOn(), it.updatedOn())) @@ -104,41 +106,41 @@ public void removeItem(String cartId, String tcin) { cartDatabase.updateCart(cartId, storedCartLines); } - public void addItem(String cartId, String tcin, Integer quantity) { + public void addItem(String cartId, String itemId, Integer quantity) { List storedCartLines = new ArrayList<>(cartDatabase.getCart(cartId)); if (storedCartLines.isEmpty()) { - throw new RuntimeException("No cart found with id " + cartId); + throw new CartNotFoundException(cartId); } - Optional storedCartLineForTcin = storedCartLines.stream().filter( it -> it.tcin().equals(tcin)).findFirst(); + Optional storedCartLineForItemId = storedCartLines.stream().filter(it -> it.itemId().equals(itemId)).findFirst(); Integer quantityForNewStoredLine = quantity; - if(storedCartLineForTcin.isPresent()) { - quantityForNewStoredLine += storedCartLineForTcin.get().quantity() ; - storedCartLines.remove(storedCartLineForTcin.get()); + if(storedCartLineForItemId.isPresent()) { + quantityForNewStoredLine += storedCartLineForItemId.get().quantity(); + storedCartLines.remove(storedCartLineForItemId.get()); } StoredCartLine storedCartLine = - new StoredCartLine(storedCartLineForTcin.map(StoredCartLine::lineId).orElseGet(() -> cartId + "-" + tcin), - cartId, tcin, quantityForNewStoredLine, ZonedDateTime.now(), ZonedDateTime.now()); + new StoredCartLine(storedCartLineForItemId.map(StoredCartLine::lineId).orElseGet(() -> cartId + "-" + itemId), + cartId, itemId, quantityForNewStoredLine, ZonedDateTime.now(), ZonedDateTime.now()); storedCartLines.add(storedCartLine); cartDatabase.updateCart(cartId, storedCartLines); } - public void updateCartItem(String cartId, String tcin, Integer quantity) { + public void updateCartItem(String cartId, String itemId, Integer quantity) { List storedCartLines = new ArrayList<>(cartDatabase.getCart(cartId)); - Optional storedCartLineForTcin = storedCartLines.stream().filter( it -> it.tcin().equals(tcin)).findFirst(); - if(storedCartLineForTcin.isPresent()) { - storedCartLines.remove(storedCartLineForTcin.get()); + Optional storedCartLineForItemId = storedCartLines.stream().filter(it -> it.itemId().equals(itemId)).findFirst(); + if(storedCartLineForItemId.isPresent()) { + storedCartLines.remove(storedCartLineForItemId.get()); if(quantity > 0) { StoredCartLine storedCartLine = - new StoredCartLine(storedCartLineForTcin.get().lineId(), - cartId, tcin, quantity, ZonedDateTime.now(), ZonedDateTime.now()); + new StoredCartLine(storedCartLineForItemId.get().lineId(), + cartId, itemId, quantity, ZonedDateTime.now(), ZonedDateTime.now()); storedCartLines.add(storedCartLine); } cartDatabase.updateCart(cartId, storedCartLines); } else { - throw new RuntimeException("No cart line found for tcin "+tcin); + throw new CartLineItemNotFoundException(itemId); } } @@ -148,36 +150,36 @@ private BigDecimal calculateDeliveryCharge(List cartLineItemList) return deliveryChargeCalculator.calculateDeliveryCharges(itemMap); } private CartLineItem assembleCartLineItem(StoredCartLine scl) { - Item item = getItem(scl.tcin()); - Price price = getPriceForItem(scl.tcin()); + Item item = getItem(scl.itemId()); + Price price = getPriceForItem(scl.itemId()); return new CartLineItem(scl.lineId(), item, scl.quantity(), price, scl.createdOn(), scl.updatedOn()); } - private Price getPriceForItem(String tcin) { - PriceApiResponse priceResponse = priceApiClient.getPricing(tcin); + private Price getPriceForItem(String itemId) { + PriceApiResponse priceResponse = priceApiClient.getPricing(itemId); if(priceResponse.priceType().equals("SALE")) { - return new Price(tcin, priceResponse.regularPrice(), Optional.of(priceResponse.salePrice())); + return new Price(itemId, priceResponse.regular(), Optional.of(priceResponse.sale())); } else { - return new Price(tcin, priceResponse.regularPrice(), Optional.empty()); + return new Price(itemId, priceResponse.regular(), Optional.empty()); } } - private Item getItem(String tcin) { - ItemApiResponse itemApiResponse = itemApiClient.getItem(tcin); + private Item getItem(String itemId) { + ItemApiResponse itemApiResponse = itemApiClient.getItem(itemId); return new Item(itemApiResponse.itemId(), itemApiResponse.smallDescription(), itemApiResponse.longDescription(), itemApiResponse.brandName(), itemApiResponse.category(), - itemApiResponse.merchClass().toString(), - itemApiResponse.imageData().primaryImage(), - itemApiResponse.imageData().alternateImage(), + itemApiResponse.merchClass(), + itemApiResponse.imageData().primary(), + itemApiResponse.imageData().alternate(), itemApiResponse.imageData().baseUrl()); } diff --git a/cart-service/src/main/java/com/target/retail/cart/service/behavior/Behaviors.java b/cart-service/src/main/java/com/target/retail/cart/service/behavior/Behaviors.java index ad13d9e..58f1b08 100644 --- a/cart-service/src/main/java/com/target/retail/cart/service/behavior/Behaviors.java +++ b/cart-service/src/main/java/com/target/retail/cart/service/behavior/Behaviors.java @@ -1,5 +1,6 @@ package com.target.retail.cart.service.behavior; +import com.target.retail.cart.exception.InducedFailureException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -76,7 +77,7 @@ private T callWithNoInducedBehavior(Supplier supplier) { private T callWithRandomFailures(Supplier supplier) { if(random.nextDouble() < failingBehaviorFailureRate) { - throw new RuntimeException("Failure to call the service. Please try later"); + throw new InducedFailureException("Failure to call the service. Please try later"); } return supplier.get(); } diff --git a/cart-service/src/main/java/com/target/retail/cart/service/client/ItemApiClient.java b/cart-service/src/main/java/com/target/retail/cart/service/client/ItemApiClient.java index c33fa8f..d4f7639 100644 --- a/cart-service/src/main/java/com/target/retail/cart/service/client/ItemApiClient.java +++ b/cart-service/src/main/java/com/target/retail/cart/service/client/ItemApiClient.java @@ -14,9 +14,9 @@ public ItemApiClient(@Value("${clients.item-service.base-url}") String baseUrl) this.restClient = RestClient.builder().baseUrl(baseUrl).build(); } - public ItemApiResponse getItem(String tcin) { + public ItemApiResponse getItem(String itemId) { return restClient.get() - .uri("/items/{item_id}", tcin) + .uri("/items/{itemId}", itemId) .retrieve() .body(ItemApiResponse.class); } diff --git a/cart-service/src/main/java/com/target/retail/cart/service/client/PriceApiClient.java b/cart-service/src/main/java/com/target/retail/cart/service/client/PriceApiClient.java index 1fdbff3..6e964b8 100644 --- a/cart-service/src/main/java/com/target/retail/cart/service/client/PriceApiClient.java +++ b/cart-service/src/main/java/com/target/retail/cart/service/client/PriceApiClient.java @@ -14,9 +14,9 @@ public PriceApiClient(@Value("${clients.price-service.base-url}") String baseUrl this.restClient = RestClient.builder().baseUrl(baseUrl).build(); } - public PriceApiResponse getPricing(String tcin) { + public PriceApiResponse getPricing(String itemId) { return restClient.get() - .uri("/prices/{item_id}", tcin) + .uri("/prices/{itemId}", itemId) .retrieve() .body(PriceApiResponse.class); } diff --git a/cart-service/src/main/java/com/target/retail/cart/service/client/dto/ItemApiResponse.java b/cart-service/src/main/java/com/target/retail/cart/service/client/dto/ItemApiResponse.java index 14e7ab1..6167f65 100644 --- a/cart-service/src/main/java/com/target/retail/cart/service/client/dto/ItemApiResponse.java +++ b/cart-service/src/main/java/com/target/retail/cart/service/client/dto/ItemApiResponse.java @@ -16,7 +16,7 @@ public record ItemApiResponse(String itemId, ImageData imageData) { @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - public record ImageData(String primaryImage, String alternateImage, String baseUrl) { + public record ImageData(String primary, String alternate, String baseUrl) { } } diff --git a/cart-service/src/main/java/com/target/retail/cart/service/client/dto/PriceApiResponse.java b/cart-service/src/main/java/com/target/retail/cart/service/client/dto/PriceApiResponse.java index 4435064..26821b2 100644 --- a/cart-service/src/main/java/com/target/retail/cart/service/client/dto/PriceApiResponse.java +++ b/cart-service/src/main/java/com/target/retail/cart/service/client/dto/PriceApiResponse.java @@ -6,4 +6,4 @@ import java.math.BigDecimal; @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -public record PriceApiResponse(String productId, BigDecimal regularPrice, BigDecimal salePrice, String priceType) {} +public record PriceApiResponse(String itemId, BigDecimal regular, BigDecimal sale, String priceType) {} diff --git a/cart-service/src/main/resources/application.yml b/cart-service/src/main/resources/application.yml index f3e084f..8c46796 100644 --- a/cart-service/src/main/resources/application.yml +++ b/cart-service/src/main/resources/application.yml @@ -12,9 +12,9 @@ cart: clients: price-service: - base-url: http://data:8080/retail_data_services/v1 + base-url: http://product-api:8080/v1 item-service: - base-url: http://data:8080/retail_data_services/v1 + base-url: http://product-api:8080/v1 taxes: defaultRate: 7.23 diff --git a/cart-service/src/main/resources/data/100.csv b/cart-service/src/main/resources/data/100.csv index 7253158..c39aa30 100644 --- a/cart-service/src/main/resources/data/100.csv +++ b/cart-service/src/main/resources/data/100.csv @@ -1,4 +1,4 @@ -lineId,cartId,tcin,quantity,createdOn,updatedOn +lineId,cartId,itemId,quantity,createdOn,updatedOn 10001,100,123456,1,2025-03-16T00:14:45.73Z,2025-03-16T00:15:45.73Z 10002,100,789123,11,2025-03-14T00:11:45.73Z,2025-03-14T00:15:45.73Z 10003,100,456788,3,2025-03-13T00:15:45.73Z,2025-03-15T00:15:45.73Z diff --git a/cart-service/src/main/resources/data/101.csv b/cart-service/src/main/resources/data/101.csv index cce9242..0b48cf1 100644 --- a/cart-service/src/main/resources/data/101.csv +++ b/cart-service/src/main/resources/data/101.csv @@ -1,4 +1,4 @@ -lineId,cartId,tcin,quantity,createdOn,updatedOn +lineId,cartId,itemId,quantity,createdOn,updatedOn 10004,101,123456,6,2025-03-10T01:15:45.73Z,2025-03-13T00:10:45.73Z 10005,101,456788,3,2025-03-11T02:15:45.73Z,2025-03-14T00:16:45.73Z 10006,101,987612,30,2025-03-12T03:15:45.73Z,2025-03-15T00:11:45.73Z diff --git a/cart-service/src/test/java/com/target/retail/cart/controller/CartControllerTest.java b/cart-service/src/test/java/com/target/retail/cart/controller/CartControllerTest.java index a387807..c596c45 100644 --- a/cart-service/src/test/java/com/target/retail/cart/controller/CartControllerTest.java +++ b/cart-service/src/test/java/com/target/retail/cart/controller/CartControllerTest.java @@ -12,11 +12,13 @@ import com.target.retail.cart.service.behavior.InducedBehavior; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.target.retail.cart.exception.CartNotFoundException; +import com.target.retail.cart.exception.CartLineItemNotFoundException; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.springframework.http.ResponseEntity; +import org.springframework.http.ResponseEntity; import java.math.BigDecimal; import java.time.ZonedDateTime; import java.util.List; @@ -25,6 +27,7 @@ import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; public class CartControllerTest { @@ -118,7 +121,6 @@ public void testRemoveLastItemFromCart() { // Mock behavior when(cartService.getCart("cart123")) - .thenReturn(Optional.of(mockCart)) .thenReturn(Optional.empty()); when(behaviors.getConfiguredBehavior()).thenReturn(createInducedBehavior()); @@ -131,22 +133,17 @@ public void testRemoveLastItemFromCart() { @Test public void testRemoveItemFromCartWhenCartDoesNotExist() { - // Mock behavior - when(cartService.getCart("cart123")).thenReturn(Optional.empty()); when(behaviors.getConfiguredBehavior()).thenReturn(createInducedBehavior()); + org.mockito.Mockito.doThrow(new CartNotFoundException("cart123")).when(cartService).removeItem("cart123", "item1"); - // Execute the method - ResponseEntity response = cartController.removeItemFromCart("cart123", "item1"); - - // Verify the response - assertEquals(ResponseEntity.notFound().build(), response); + assertThrows(CartNotFoundException.class, () -> cartController.removeItemFromCart("cart123", "item1")); } @Test public void testUpdateCartItem() { // Mock data String cartId = "cart123"; - String tcin = "item1"; + String itemId = "item1"; int newQuantity = 5; Cart mockCart = new Cart( cartId, @@ -160,7 +157,7 @@ public void testUpdateCartItem() { when(behaviors.getConfiguredBehavior()).thenReturn(createInducedBehavior()); // Execute the method - ResponseEntity response = cartController.updateItem(cartId, tcin, new UpdateItemRequest(newQuantity)); + ResponseEntity response = cartController.updateItem(cartId, itemId, new UpdateItemRequest(newQuantity)); // Verify the response assertEquals(ResponseEntity.ok(CartResponse.from(mockCart)), response); @@ -168,44 +165,26 @@ public void testUpdateCartItem() { @Test public void testUpdateCartItemWhenCartDoesNotExist() { - // Mock data String cartId = "cart123"; - String tcin = "item1"; + String itemId = "item1"; int newQuantity = 5; - // Mock behavior - when(cartService.getCart(cartId)).thenReturn(Optional.empty()); when(behaviors.getConfiguredBehavior()).thenReturn(createInducedBehavior()); + org.mockito.Mockito.doThrow(new CartNotFoundException(cartId)).when(cartService).updateCartItem(cartId, itemId, newQuantity); - // Execute the method - ResponseEntity response = cartController.updateItem(cartId, tcin, new UpdateItemRequest(newQuantity)); - - // Verify the response - assertEquals(ResponseEntity.notFound().build(), response); + assertThrows(CartNotFoundException.class, () -> cartController.updateItem(cartId, itemId, new UpdateItemRequest(newQuantity))); } @Test - public void testUpdateCartItemWhenTcinNotFoundInCart() { - // Mock data + public void testUpdateCartItemWhenItemIdNotFoundInCart() { String cartId = "cart123"; - String tcin = "missingItemId"; + String itemId = "missingItemId"; int newQuantity = 5; - Cart mockCart = new Cart( - cartId, - new BigDecimal("10"), - new BigDecimal("10"), - List.of(newCartLineItem()) - ); - // Mock behavior - when(cartService.getCart(cartId)).thenReturn(Optional.of(mockCart)); when(behaviors.getConfiguredBehavior()).thenReturn(createInducedBehavior()); + org.mockito.Mockito.doThrow(new CartLineItemNotFoundException(itemId)).when(cartService).updateCartItem(cartId, itemId, newQuantity); - // Execute the method - ResponseEntity response = cartController.updateItem(cartId, tcin, new UpdateItemRequest(newQuantity)); - - // Verify the response - assertEquals(ResponseEntity.notFound().build(), response); + assertThrows(CartLineItemNotFoundException.class, () -> cartController.updateItem(cartId, itemId, new UpdateItemRequest(newQuantity))); } @Test @@ -240,7 +219,7 @@ public void testCreateCartWithDuplicateTcins() { // Mock data List addItems = List.of( new AddItemRequest("item1", 2), - new AddItemRequest("item1", 3) // Duplicate tcin + new AddItemRequest("item1", 3) // Duplicate item ID ); // Execute the method @@ -266,7 +245,7 @@ private CartLineItem newCartLineItem(String suffix) { String itemId = "item%s".formatted(suffix); return new CartLineItem(lineId, - new Item(itemId, "Small Description", "Long Description", "Brand", "Category", "Class", "primaryImage", "alternateImage", "baseUrl"), 1, new Price(itemId, new BigDecimal("10"), Optional.empty()), ZonedDateTime.now(), ZonedDateTime.now()); + new Item(itemId, "Small Description", "Long Description", "Brand", "Category", 12, "primary", "alternate", "baseUrl"), 1, new Price(itemId, new BigDecimal("10"), Optional.empty()), ZonedDateTime.now(), ZonedDateTime.now()); } private CartLineItem newCartLineItem() { diff --git a/cart-service/src/test/java/com/target/retail/cart/controller/dto/CartResponseSerializationTest.java b/cart-service/src/test/java/com/target/retail/cart/controller/dto/CartResponseSerializationTest.java index e7efc99..057484a 100644 --- a/cart-service/src/test/java/com/target/retail/cart/controller/dto/CartResponseSerializationTest.java +++ b/cart-service/src/test/java/com/target/retail/cart/controller/dto/CartResponseSerializationTest.java @@ -33,7 +33,7 @@ public void timestampsSerializeAsIso8601Strings() throws Exception { new BigDecimal("5.00"), List.of(new CartLineItem( "1", - new Item("12345", "Title", "Description", "Brand", "APPAREL", "MerchClass", + new Item("12345", "Title", "Description", "Brand", "APPAREL", 12, "primary", "alternate", "baseUrl"), 1, new Price("12345", new BigDecimal("10.00"), Optional.empty()), diff --git a/cart-service/src/test/java/com/target/retail/cart/controller/dto/CartResponseTest.java b/cart-service/src/test/java/com/target/retail/cart/controller/dto/CartResponseTest.java index 6d1bc4a..203d123 100644 --- a/cart-service/src/test/java/com/target/retail/cart/controller/dto/CartResponseTest.java +++ b/cart-service/src/test/java/com/target/retail/cart/controller/dto/CartResponseTest.java @@ -23,7 +23,7 @@ public void testFrom() { "cart123", new BigDecimal("10"), new BigDecimal("10"), - List.of(new CartLineItem("1",new Item("12345", "Item Title", "Item Description", "Brand", "Category", "MerchClass", "primaryImage", "alternateImage", "baseUrl"), 2, new Price("12345", new BigDecimal("8.00"), Optional.of(new BigDecimal("6.00"))), + List.of(new CartLineItem("1",new Item("12345", "Item Title", "Item Description", "Brand", "Category", 12, "primary", "alternate", "baseUrl"), 2, new Price("12345", new BigDecimal("8.00"), Optional.of(new BigDecimal("6.00"))), ZonedDateTime.now(), ZonedDateTime.now()))); @@ -43,16 +43,16 @@ public void testFrom() { assertEquals(cart.cartLineItems().size(), cartResponse.items().size()); CartLineItem cartLineItem = cart.cartLineItems().get(0); CartResponse.ItemResponse itemResponse = cartResponse.items().get(0); - assertEquals(cartLineItem.item().tcin(), itemResponse.tcin()); + assertEquals(cartLineItem.item().itemId(), itemResponse.itemId()); assertEquals(cartLineItem.item().title(), itemResponse.title()); assertEquals(cartLineItem.item().description(), itemResponse.description()); assertEquals(cartLineItem.item().brand(), itemResponse.brand()); assertEquals(cartLineItem.item().merchClass(), itemResponse.merchClass()); assertEquals(cartLineItem.quantity(), itemResponse.quantity()); - assertEquals(cartLineItem.price().regularPrice(), itemResponse.price().regular()); - assertEquals(cartLineItem.price().salePrice().orElse(null), itemResponse.price().sale()); - assertEquals(cartLineItem.item().primaryImage(), itemResponse.imageData().primary()); - assertEquals(cartLineItem.item().alternateImage(), itemResponse.imageData().alternate()); + assertEquals(cartLineItem.price().regular(), itemResponse.price().regular()); + assertEquals(cartLineItem.price().sale().orElse(null), itemResponse.price().sale()); + assertEquals(cartLineItem.item().primary(), itemResponse.imageData().primary()); + assertEquals(cartLineItem.item().alternate(), itemResponse.imageData().alternate()); assertEquals(cartLineItem.item().baseUrl(), itemResponse.imageData().baseUrl()); } @@ -64,7 +64,7 @@ public void testFormattedPricesInCartResponse() { new BigDecimal("5.25"), List.of(new CartLineItem( "1", - new Item("12345", "Item Title", "Item Description", "Brand", "Category", "MerchClass", "primaryImage", "alternateImage", "baseUrl"), + new Item("12345", "Item Title", "Item Description", "Brand", "Category", 12, "primary", "alternate", "baseUrl"), 2, new Price("12345", new BigDecimal("8.00"), Optional.of(new BigDecimal("6.50"))), ZonedDateTime.now(), diff --git a/cart-service/src/test/java/com/target/retail/cart/data/CartDatabaseTest.java b/cart-service/src/test/java/com/target/retail/cart/data/CartDatabaseTest.java index 6802163..cf1bc8a 100644 --- a/cart-service/src/test/java/com/target/retail/cart/data/CartDatabaseTest.java +++ b/cart-service/src/test/java/com/target/retail/cart/data/CartDatabaseTest.java @@ -21,14 +21,14 @@ public class CartDatabaseTest { private static final String cartsDataPath = "./carts-test/"; private static final String testDataForCart1 = """ - lineId,cartId,tcin,quantity,createdOn,updatedOn + lineId,cartId,itemId,quantity,createdOn,updatedOn 10001,cart-1,987612,10,2025-03-10T00:14:45.73Z,2025-03-16T00:15:45.73Z 10002,cart-1,789123,12,2025-03-11T00:11:45.73Z,2025-03-14T00:15:45.73Z 10003,cart-1,456788,1,2025-03-09T00:15:45.73Z,2025-03-15T00:15:45.73Z """ ; private static final String testDataForCart2 = """ - lineId,cartId,tcin,quantity,createdOn,updatedOn + lineId,cartId,itemId,quantity,createdOn,updatedOn 10004,cart-2,987612,1,2025-03-16T00:14:45.73Z,2025-03-16T00:15:45.73Z 10005,cart-2,789123,2,2025-03-14T00:11:45.73Z,2025-03-14T00:15:45.73Z 10006,cart-2,456788,3,2025-03-13T00:15:45.73Z,2025-03-15T00:15:45.73Z diff --git a/cart-service/src/test/java/com/target/retail/cart/service/CartServiceTest.java b/cart-service/src/test/java/com/target/retail/cart/service/CartServiceTest.java index e3bda68..1dba664 100644 --- a/cart-service/src/test/java/com/target/retail/cart/service/CartServiceTest.java +++ b/cart-service/src/test/java/com/target/retail/cart/service/CartServiceTest.java @@ -1,6 +1,7 @@ package com.target.retail.cart.service; import com.target.retail.cart.data.CartDatabase; +import com.target.retail.cart.exception.CartLineItemNotFoundException; import com.target.retail.cart.model.Cart; import com.target.retail.cart.model.CartLineItem; import com.target.retail.cart.model.Item; @@ -50,18 +51,18 @@ public void setUp() { public void testGetCart() { // Mock data String cartId = "123"; - String tcin = "456"; + String itemId = "456"; String deliveryZip = "78910"; - StoredCartLine storedCartLine = new StoredCartLine("1", cartId, tcin, 2, ZonedDateTime.now(), ZonedDateTime.now()); + StoredCartLine storedCartLine = new StoredCartLine("1", cartId, itemId, 2, ZonedDateTime.now(), ZonedDateTime.now()); List storedCartLines = List.of(storedCartLine); - Item item = new Item(tcin, "Short description", "Long description", "Brand", "Category", "MerchClass", "PrimaryImage", "AlternateImage", "BaseUrl"); - Price price = new Price(tcin, BigDecimal.valueOf(10.00), Optional.of(BigDecimal.valueOf(8.00))); + Item item = new Item(itemId, "Short description", "Long description", "Brand", "Category", 12, "PrimaryImage", "AlternateImage", "BaseUrl"); + Price price = new Price(itemId, BigDecimal.valueOf(10.00), Optional.of(BigDecimal.valueOf(8.00))); CartLineItem cartLineItem = new CartLineItem("1", item, 2, price, ZonedDateTime.now(), ZonedDateTime.now()); when(cartDatabase.getCart(cartId)).thenReturn(storedCartLines); - when(itemApiClient.getItem(anyString())).thenReturn(new ItemApiResponse(tcin, "Short description", "Long description", "Brand", 5, "ONLINE", "BAR12345", "BRAND", 21, new ItemApiResponse.ImageData("PrimaryImage", "AlternateImage", "BaseUrl"))); - when(priceApiClient.getPricing(anyString())).thenReturn(new PriceApiResponse(tcin, BigDecimal.valueOf(10.00), BigDecimal.valueOf(8.00), "SALE")); + when(itemApiClient.getItem(anyString())).thenReturn(new ItemApiResponse(itemId, "Short description", "Long description", "Brand", 5, "ONLINE", "BAR12345", "BRAND", 21, new ItemApiResponse.ImageData("PrimaryImage", "AlternateImage", "BaseUrl"))); + when(priceApiClient.getPricing(anyString())).thenReturn(new PriceApiResponse(itemId, BigDecimal.valueOf(10.00), BigDecimal.valueOf(8.00), "SALE")); when(taxCalculator.calculateTax(Mockito.any(BigDecimal.class), Mockito.anyString())).thenReturn(BigDecimal.valueOf(1.00)); when(deliveryChargeCalculator.calculateDeliveryCharges(Mockito.anyMap())).thenReturn(BigDecimal.valueOf(5.00)); @@ -74,31 +75,31 @@ public void testGetCart() { assertEquals(BigDecimal.valueOf(1.00), cart.get().totalTax()); assertEquals(BigDecimal.valueOf(5.00), cart.get().deliveryCharges()); assertEquals(1, cart.get().cartLineItems().size()); - assertEquals(cartLineItem.item().tcin(), cart.get().cartLineItems().get(0).item().tcin()); + assertEquals(cartLineItem.item().itemId(), cart.get().cartLineItems().get(0).item().itemId()); } @Test public void testRemoveItem() { // Mock data String cartId = "123"; - String tcin1 = "456"; - String tcin2 = "123"; + String itemId1 = "456"; + String itemId2 = "123"; String deliveryZip = "78910"; ZonedDateTime now = ZonedDateTime.now(); - StoredCartLine storedCartLine1 = new StoredCartLine("1", cartId, tcin1, 2, now, now); - StoredCartLine storedCartLine2 = new StoredCartLine("2", cartId, tcin2, 10, now, now); + StoredCartLine storedCartLine1 = new StoredCartLine("1", cartId, itemId1, 2, now, now); + StoredCartLine storedCartLine2 = new StoredCartLine("2", cartId, itemId2, 10, now, now); List storedCartLines = List.of(storedCartLine1, storedCartLine2); - Item item = new Item(tcin1, "Short description", "Long description", "Brand", "Category", "MerchClass", "PrimaryImage", "AlternateImage", "BaseUrl"); - Price price = new Price(tcin1, BigDecimal.valueOf(10.00), Optional.of(BigDecimal.valueOf(8.00))); + Item item = new Item(itemId1, "Short description", "Long description", "Brand", "Category", 12, "PrimaryImage", "AlternateImage", "BaseUrl"); + Price price = new Price(itemId1, BigDecimal.valueOf(10.00), Optional.of(BigDecimal.valueOf(8.00))); CartLineItem cartLineItem = new CartLineItem("1", item, 2, price, ZonedDateTime.now(), ZonedDateTime.now()); when(cartDatabase.getCart(cartId)).thenReturn(storedCartLines); - when(itemApiClient.getItem(tcin1)).thenReturn(new ItemApiResponse(tcin1, "Short description1", "Long description1", "Brand1", 5, "ONLINE", "BAR12345", "BRAND", 21, new ItemApiResponse.ImageData("PrimaryImage", "AlternateImage", "BaseUrl"))); - when(itemApiClient.getItem(tcin2)).thenReturn(new ItemApiResponse(tcin2, "Short description2", "Long description2", "Brand2", 5, "ONLINE", "BAR12345", "BRAND", 1, new ItemApiResponse.ImageData("PrimaryImage", "AlternateImage", "BaseUrl"))); + when(itemApiClient.getItem(itemId1)).thenReturn(new ItemApiResponse(itemId1, "Short description1", "Long description1", "Brand1", 5, "ONLINE", "BAR12345", "BRAND", 21, new ItemApiResponse.ImageData("PrimaryImage", "AlternateImage", "BaseUrl"))); + when(itemApiClient.getItem(itemId2)).thenReturn(new ItemApiResponse(itemId2, "Short description2", "Long description2", "Brand2", 5, "ONLINE", "BAR12345", "BRAND", 1, new ItemApiResponse.ImageData("PrimaryImage", "AlternateImage", "BaseUrl"))); - when(priceApiClient.getPricing(anyString())).thenReturn(new PriceApiResponse(tcin1, BigDecimal.valueOf(10.00), BigDecimal.valueOf(8.00), "SALE")); + when(priceApiClient.getPricing(anyString())).thenReturn(new PriceApiResponse(itemId1, BigDecimal.valueOf(10.00), BigDecimal.valueOf(8.00), "SALE")); when(taxCalculator.calculateTax(Mockito.any(BigDecimal.class), Mockito.anyString())).thenReturn(BigDecimal.valueOf(1.00)); when(deliveryChargeCalculator.calculateDeliveryCharges(Mockito.anyMap())).thenReturn(BigDecimal.valueOf(5.00)); @@ -107,68 +108,68 @@ public void testRemoveItem() { assertEquals(2, cart.get().cartLineItems().size()); // Call the method to remove item from cart - cartService.removeItem(cartId, tcin1); + cartService.removeItem(cartId, itemId1); // verify the call to updateCart - Mockito.verify(cartDatabase).updateCart(cartId, List.of( new StoredCartLine("2", cartId, tcin2, 10, now, now))); + Mockito.verify(cartDatabase).updateCart(cartId, List.of( new StoredCartLine("2", cartId, itemId2, 10, now, now))); } @Test - public void testAddItem_CartHasOneLineBeforeAddingNewOneAndTCINisNew() { + public void testAddItem_CartHasOneLineBeforeAddingNewOneAndItemIdIsNew() { // Mock data String cartId = "123"; - String tcin = "456"; - String existingTcin = "789"; + String itemId = "456"; + String existingItemId = "789"; Integer quantity = 2; ZonedDateTime now = ZonedDateTime.now(); List storedCartLines = List.of( - new StoredCartLine("1", cartId, existingTcin, 1, now, now) + new StoredCartLine("1", cartId, existingItemId, 1, now, now) ); when(cartDatabase.getCart(cartId)).thenReturn(storedCartLines); // Call the method to add item to cart - cartService.addItem(cartId, tcin, quantity); + cartService.addItem(cartId, itemId, quantity); // Verify the call to updateCart Mockito.verify(cartDatabase).updateCart(eq(cartId), argThat(list -> list.size() == 2 && list.get(0).getId().equals("1") && list.get(0).cartId().equals(cartId) && - list.get(0).tcin().equals(existingTcin) && + list.get(0).itemId().equals(existingItemId) && list.get(0).quantity() == 1 && - list.get(1).getId().equals(cartId+"-"+tcin) && + list.get(1).getId().equals(cartId+"-"+itemId) && list.get(1).cartId().equals(cartId) && - list.get(1).tcin().equals(tcin) && + list.get(1).itemId().equals(itemId) && Objects.equals(list.get(1).quantity(), quantity) )); } @Test - public void testAddItem_CartHasExistingTCIN_QuantitiesUpdated() { + public void testAddItem_CartHasExistingItemId_QuantitiesUpdated() { // Mock data String cartId = "123"; - String tcin = "456"; + String itemId = "456"; Integer existingQuantity = 2; Integer additionalQuantity = 3; ZonedDateTime now = ZonedDateTime.now(); List storedCartLines = List.of( - new StoredCartLine("1", cartId, tcin, existingQuantity, now, now) + new StoredCartLine("1", cartId, itemId, existingQuantity, now, now) ); when(cartDatabase.getCart(cartId)).thenReturn(storedCartLines); // Call the method to add item to cart - cartService.addItem(cartId, tcin, additionalQuantity); + cartService.addItem(cartId, itemId, additionalQuantity); // Verify the call to updateCart Mockito.verify(cartDatabase).updateCart(eq(cartId), argThat(list -> list.size() == 1 && list.get(0).getId().equals("1") && list.get(0).cartId().equals(cartId) && - list.get(0).tcin().equals(tcin) && + list.get(0).itemId().equals(itemId) && list.get(0).quantity() == (existingQuantity + additionalQuantity) )); } @@ -178,23 +179,23 @@ public void testAddItem_CartHasExistingTCIN_QuantitiesUpdated() { public void testUpdateCartItem_ItemFoundInCart() { // Mock data String cartId = "123"; - String tcin = "456"; + String itemId = "456"; Integer newQuantity = 5; ZonedDateTime now = ZonedDateTime.now(); - StoredCartLine storedCartLine = new StoredCartLine("1", cartId, tcin, 2, now, now); + StoredCartLine storedCartLine = new StoredCartLine("1", cartId, itemId, 2, now, now); List storedCartLines = List.of(storedCartLine); when(cartDatabase.getCart(cartId)).thenReturn(storedCartLines); // Call the method to update the cart item - cartService.updateCartItem(cartId, tcin, newQuantity); + cartService.updateCartItem(cartId, itemId, newQuantity); // Verify the call to updateCart Mockito.verify(cartDatabase).updateCart(eq(cartId), argThat(list -> list.size() == 1 && list.get(0).getId().equals("1") && list.get(0).cartId().equals(cartId) && - list.get(0).tcin().equals(tcin) && + list.get(0).itemId().equals(itemId) && Objects.equals(list.get(0).quantity(), newQuantity) )); } @@ -203,47 +204,47 @@ public void testUpdateCartItem_ItemFoundInCart() { public void testUpdateCartItem_ItemNotFoundInCart() { // Mock data String cartId = "123"; - String tcin = "456"; + String itemId = "456"; Integer newQuantity = 5; // Mock an empty cart or a cart without the specified TCIN when(cartDatabase.getCart(cartId)).thenReturn(List.of()); - // Assert that the method throws a RuntimeException - RuntimeException exception = org.junit.jupiter.api.Assertions.assertThrows( - RuntimeException.class, - () -> cartService.updateCartItem(cartId, tcin, newQuantity) + // Assert that the method throws a CartLineItemNotFoundException + CartLineItemNotFoundException exception = org.junit.jupiter.api.Assertions.assertThrows( + CartLineItemNotFoundException.class, + () -> cartService.updateCartItem(cartId, itemId, newQuantity) ); // Verify the exception message - assertEquals("No cart line found for tcin " + tcin, exception.getMessage()); + assertEquals("No cart line found for item id " + itemId, exception.getMessage()); } @Test public void testUpdateCartItem_QuantityZero_ItemRemoved() { // Mock data String cartId = "123"; - String tcinToRemove = "456"; - String tcinToKeep = "789"; + String itemIdToRemove = "456"; + String itemIdToKeep = "789"; Integer quantityToRemove = 0; Integer quantityToKeep = 5; ZonedDateTime now = ZonedDateTime.now(); - StoredCartLine lineToRemove = new StoredCartLine("1", cartId, tcinToRemove, 2, now, now); - StoredCartLine lineToKeep = new StoredCartLine("2", cartId, tcinToKeep, quantityToKeep, now, now); + StoredCartLine lineToRemove = new StoredCartLine("1", cartId, itemIdToRemove, 2, now, now); + StoredCartLine lineToKeep = new StoredCartLine("2", cartId, itemIdToKeep, quantityToKeep, now, now); List storedCartLines = List.of(lineToRemove, lineToKeep); when(cartDatabase.getCart(cartId)).thenReturn(storedCartLines); // Call the method to update the cart item - cartService.updateCartItem(cartId, tcinToRemove, quantityToRemove); + cartService.updateCartItem(cartId, itemIdToRemove, quantityToRemove); // Verify the call to updateCart Mockito.verify(cartDatabase).updateCart(eq(cartId), argThat(list -> list.size() == 1 && list.get(0).getId().equals("2") && list.get(0).cartId().equals(cartId) && - list.get(0).tcin().equals(tcinToKeep) && + list.get(0).itemId().equals(itemIdToKeep) && Objects.equals(list.get(0).quantity(), quantityToKeep) )); } @@ -252,8 +253,8 @@ public void testUpdateCartItem_QuantityZero_ItemRemoved() { public void testCreateCart() { // Mock data String cartId = "123"; - String tcin1 = "456"; - String tcin2 = "789"; + String itemId1 = "456"; + String itemId2 = "789"; Integer quantity1 = 2; Integer quantity2 = 3; @@ -261,19 +262,19 @@ public void testCreateCart() { when(cartDatabase.newCartId()).thenReturn(cartId); // Call the method to create a cart - cartService.createCart(Map.of(tcin1, quantity1, tcin2, quantity2)); + cartService.createCart(Map.of(itemId1, quantity1, itemId2, quantity2)); // Verify the call to updateCart Mockito.verify(cartDatabase).updateCart(eq(cartId), argThat(list -> list.size() == 2 && list.stream().anyMatch(line -> line.cartId().equals(cartId) && - line.tcin().equals(tcin1) && + line.itemId().equals(itemId1) && Objects.equals(line.quantity(), quantity1) ) && list.stream().anyMatch(line -> line.cartId().equals(cartId) && - line.tcin().equals(tcin2) && + line.itemId().equals(itemId2) && Objects.equals(line.quantity(), quantity2) ) )); diff --git a/cart-service/src/test/resources/application.yml b/cart-service/src/test/resources/application.yml index 98e1e94..57d5c02 100644 --- a/cart-service/src/test/resources/application.yml +++ b/cart-service/src/test/resources/application.yml @@ -3,9 +3,9 @@ cart: clients: price-service: - base-url: http://data:8080/retail_data_services/v1 + base-url: http://product-api:8080/v1 item-service: - base-url: http://data:8080/retail_data_services/v1 + base-url: http://product-api:8080/v1 taxes: defaultRate: 7.23 diff --git a/docker-compose.yml b/docker-compose.yml index 9b59cf0..a97c0d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,12 @@ services: - data: + product-api: build: - context: retail-data-services + context: product-api dockerfile: Dockerfile ports: - "8080:8080" healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + test: ["CMD", "curl", "-f", "http://localhost:8080/v1/health"] interval: 5s timeout: 3s retries: 3 @@ -18,5 +18,5 @@ services: ports: - "8081:8081" depends_on: - data: + product-api: condition: service_healthy diff --git a/retail-data-services/.dockerignore b/product-api/.dockerignore similarity index 100% rename from retail-data-services/.dockerignore rename to product-api/.dockerignore diff --git a/retail-data-services/Dockerfile b/product-api/Dockerfile similarity index 86% rename from retail-data-services/Dockerfile rename to product-api/Dockerfile index 3a66aea..885a5a4 100644 --- a/retail-data-services/Dockerfile +++ b/product-api/Dockerfile @@ -4,7 +4,7 @@ FROM eclipse-temurin:17-jre@sha256:3443191793fb27f7df8bab42821d149eec9e280ab28e8 WORKDIR /app # Copy the current directory contents into the container at /app -COPY build/libs/retail-data-services.jar /app/retail-data-services.jar +COPY build/libs/product-api.jar /app/product-api.jar # Create the /data directory RUN mkdir /data @@ -25,4 +25,4 @@ ENTRYPOINT ["java", \ "-XX:TieredStopAtLevel=1", \ "-XX:+UseSerialGC", \ "-Xss256k", \ - "-jar", "/app/retail-data-services.jar"] + "-jar", "/app/product-api.jar"] diff --git a/retail-data-services/api-spec/retail_data_services-v1.yaml b/product-api/api-spec/product-api-v1.yaml similarity index 97% rename from retail-data-services/api-spec/retail_data_services-v1.yaml rename to product-api/api-spec/product-api-v1.yaml index 3e13644..0e630c0 100644 --- a/retail-data-services/api-spec/retail_data_services-v1.yaml +++ b/product-api/api-spec/product-api-v1.yaml @@ -1,10 +1,10 @@ openapi: 3.1.0 info: - title: Retail Data Services API - description: API for managing retail data services. + title: Product API + description: Read-only API for product catalog, pricing, and availability. version: "1.0" servers: -- url: http://localhost:8080/retail_data_services/v1 +- url: http://localhost:8080/v1 description: Generated server url paths: /prices/{id}: diff --git a/retail-data-services/build.gradle.kts b/product-api/build.gradle.kts similarity index 82% rename from retail-data-services/build.gradle.kts rename to product-api/build.gradle.kts index e493388..8d294c8 100644 --- a/retail-data-services/build.gradle.kts +++ b/product-api/build.gradle.kts @@ -1,9 +1,9 @@ application { - mainClass.set("com.target.retail.data.services.Main") + mainClass.set("com.target.retail.product.Main") } tasks.named("bootJar") { - archiveFileName.set("retail-data-services.jar") + archiveFileName.set("product-api.jar") } dependencies { diff --git a/retail-data-services/data-formats.md b/product-api/data-formats.md similarity index 100% rename from retail-data-services/data-formats.md rename to product-api/data-formats.md diff --git a/retail-data-services/induced_behaviors.md b/product-api/induced_behaviors.md similarity index 83% rename from retail-data-services/induced_behaviors.md rename to product-api/induced_behaviors.md index 88a2dbe..69f0922 100644 --- a/retail-data-services/induced_behaviors.md +++ b/product-api/induced_behaviors.md @@ -24,7 +24,7 @@ If `DEFAULT_BEHAVIOR` is set to anything else, the service will fail when it tri Assumptions in the examples: -- Container image name: `retail-data-services` +- Container image name: `product-api` - Container port: `8080` (mapped to different host ports as needed) ## NORMAL behavior @@ -43,7 +43,6 @@ Conceptually, controllers/services call into a function that builds the API resp - You can omit `DEFAULT_BEHAVIOR` entirely (it defaults to `NORMAL`), or - Set it explicitly: - - `DEFAULT_BEHAVIOR=NORMAL` There are no additional tuning environment variables for this behavior. @@ -55,7 +54,7 @@ Implicit default (no env var): ```bash docker run --rm \ -p 8080:8080 \ - retail-data-services + product-api ``` Explicitly set `NORMAL`: @@ -64,7 +63,7 @@ Explicitly set `NORMAL`: docker run --rm \ -p 8080:8080 \ -e DEFAULT_BEHAVIOR=NORMAL \ - retail-data-services + product-api ``` **Run with `docker-compose`** @@ -73,8 +72,8 @@ Example service definition: ```yaml services: - retail-data-services-normal: - image: retail-data-services + product-api-normal: + image: product-api ports: - "8080:8080" environment: @@ -129,16 +128,16 @@ Tune how slow API responses are: **Run with `docker run`** -Use default delay settings (around 1–10 seconds before each API response is generated): +Use default delay settings (around 1 to 10 seconds before each API response is generated): ```bash docker run --rm \ -p 8080:8080 \ -e DEFAULT_BEHAVIOR=SLOW_RESPONSE \ - retail-data-services + product-api ``` -Override delays for a milder slowdown (roughly 0.5–2 seconds added before generating each response): +Override delays for a milder slowdown (roughly 0.5 to 2 seconds added before generating each response): ```bash docker run --rm \ @@ -146,10 +145,10 @@ docker run --rm \ -e DEFAULT_BEHAVIOR=SLOW_RESPONSE \ -e BEHAVIORS_SLOW_RESPONSE_MIN_DELAY_MS=500 \ -e BEHAVIORS_SLOW_RESPONSE_MAX_DELAY_MS=2000 \ - retail-data-services + product-api ``` -Override for a heavier slowdown (roughly 3–8 seconds before generating each response): +Override for a heavier slowdown (roughly 3 to 8 seconds before generating each response): ```bash docker run --rm \ @@ -157,7 +156,7 @@ docker run --rm \ -e DEFAULT_BEHAVIOR=SLOW_RESPONSE \ -e BEHAVIORS_SLOW_RESPONSE_MIN_DELAY_MS=3000 \ -e BEHAVIORS_SLOW_RESPONSE_MAX_DELAY_MS=8000 \ - retail-data-services + product-api ``` **Run with `docker-compose`** @@ -166,8 +165,8 @@ Mild slowdown example (host port 8081): ```yaml services: - retail-data-services-slow: - image: retail-data-services + product-api-slow: + image: product-api ports: - "8081:8080" environment: @@ -180,8 +179,8 @@ Heavier slowdown variant: ```yaml services: - retail-data-services-slow-heavy: - image: retail-data-services + product-api-slow-heavy: + image: product-api ports: - "8083:8080" environment: @@ -206,10 +205,10 @@ services: - For each wrapped API call, this behavior: 1. Draws a random number between 0.0 and 1.0. 2. Compares it to a configured failure rate. - 3. If the random number is less than the failure rate, it throws an exception instead of generating the API response. + 3. If the random number is less than the failure rate, it throws an `InducedFailureException` instead of generating the API response. 4. Otherwise, it runs the normal logic that builds the API response and returns that response. -This causes a configurable fraction of requests to fail before a response is generated, usually surfacing as HTTP 5xx errors (depending on the global exception handling) instead of the normal API payload. +This causes a configurable fraction of requests to fail before a response is generated. The `GlobalExceptionHandler` maps the resulting `InducedFailureException` to HTTP 503 Service Unavailable instead of returning the normal API payload. ### Configuration and Docker usage @@ -234,7 +233,7 @@ Use the default failure rate (~5% of API responses fail): docker run --rm \ -p 8080:8080 \ -e DEFAULT_BEHAVIOR=RANDOM_FAILURES \ - retail-data-services + product-api ``` Increase the failure rate to about 25% of API calls: @@ -244,7 +243,7 @@ docker run --rm \ -p 8080:8080 \ -e DEFAULT_BEHAVIOR=RANDOM_FAILURES \ -e BEHAVIORS_RANDOM_FAILING_FAILURE_RATE=0.25 \ - retail-data-services + product-api ``` Use a very aggressive 50% failure rate (half of the API calls fail): @@ -254,17 +253,17 @@ docker run --rm \ -p 8080:8080 \ -e DEFAULT_BEHAVIOR=RANDOM_FAILURES \ -e BEHAVIORS_RANDOM_FAILING_FAILURE_RATE=0.5 \ - retail-data-services + product-api ``` **Run with `docker-compose`** -Light flakiness (~10–20% of API calls) on host port 8082: +Light flakiness (~10 to 20% of API calls) on host port 8082: ```yaml services: - retail-data-services-flaky: - image: retail-data-services + product-api-flaky: + image: product-api ports: - "8082:8080" environment: @@ -276,8 +275,8 @@ More aggressive flakiness (~50% of API calls): ```yaml services: - retail-data-services-flaky-heavy: - image: retail-data-services + product-api-flaky-heavy: + image: product-api ports: - "8084:8080" environment: @@ -302,32 +301,33 @@ services: - Wait, then generate and return the response (SLOW_RESPONSE), or - Fail early without generating a response at all (RANDOM_FAILURES). -By setting only environment variables when you start the container, you can run the same API code in any of these modes. Endpoints that don’t use the behavior wrapper will continue to generate responses normally. +By setting only environment variables when you start the container, you can run the same API code in any of these modes. Endpoints that don't use the behavior wrapper will continue to generate responses normally. ## Use cases and tips -- **UI latency testing** +- **UI latency testing** Point your UI or client at a `SLOW_RESPONSE` instance to observe loading indicators, spinners, and timeouts when API responses are delayed. -- **Resilience and retry testing** - Use a `RANDOM_FAILURES` instance with a moderate failure rate (e.g., 0.1–0.3) to exercise retries, backoff strategies, and error handling when API responses sometimes fail outright. +- **Resilience and retry testing** + Use a `RANDOM_FAILURES` instance with a moderate failure rate (e.g., 0.1 to 0.3) to exercise retries, backoff strategies, and error handling when API responses sometimes fail outright. -- **Baseline vs. stressed comparison** +- **Baseline vs. stressed comparison** Run `NORMAL` and one induced-behavior instance in parallel (on different ports) to compare metrics, logs, and user experience between normal and stressed API behavior. -- **Safety tips** - - Very high failure rates or very long delays can make the API appear "down." Start with conservative values and ramp up. +- **Safety tips** + - Very high failure rates or very long delays can make the API appear "down." Start with conservative values and ramp up. - The chosen `DEFAULT_BEHAVIOR` applies process-wide. To run different modes simultaneously, start multiple containers with different environment settings. ## References For deeper technical details, see: -- `src/main/java/com/target/retail/data/services/service/behavior/InducedBehavior.java` +- `src/main/java/com/target/retail/product/service/behavior/InducedBehavior.java` Functional interface that wraps a `Supplier` representing the API-response generation logic and lets behaviors inject cross-cutting effects. -- `src/main/java/com/target/retail/data/services/service/behavior/BehaviorType.java` +- `src/main/java/com/target/retail/product/service/behavior/BehaviorType.java` Enum declaring the supported behavior types (`NORMAL`, `SLOW_RESPONSE`, `RANDOM_FAILURES`). -- `src/main/java/com/target/retail/data/services/service/behavior/Behaviors.java` +- `src/main/java/com/target/retail/product/service/behavior/Behaviors.java` Component that reads configuration from environment variables (including `DEFAULT_BEHAVIOR`, failure rate, and delay bounds), wires behavior lambdas into a map, and exposes the configured behavior used to wrap API-response generation. + diff --git a/retail-data-services/retail-data-services.http b/product-api/product-api.http similarity index 100% rename from retail-data-services/retail-data-services.http rename to product-api/product-api.http diff --git a/retail-data-services/scripts/README.md b/product-api/scripts/README.md similarity index 89% rename from retail-data-services/scripts/README.md rename to product-api/scripts/README.md index b0d4acf..e9fb7b2 100644 --- a/retail-data-services/scripts/README.md +++ b/product-api/scripts/README.md @@ -2,7 +2,7 @@ ## benchmark-startup.sh -Measures Docker container startup time for retail-data-services. +Measures Docker container startup time for product-api. ### Usage diff --git a/retail-data-services/scripts/benchmark-startup.sh b/product-api/scripts/benchmark-startup.sh similarity index 92% rename from retail-data-services/scripts/benchmark-startup.sh rename to product-api/scripts/benchmark-startup.sh index ea7e2de..a005418 100755 --- a/retail-data-services/scripts/benchmark-startup.sh +++ b/product-api/scripts/benchmark-startup.sh @@ -1,7 +1,7 @@ #!/bin/bash # ============================================================================ -# Retail Data Services - Docker Startup Time Benchmark Script +# Product API - Docker Startup Time Benchmark Script # ============================================================================ # Measures Docker container startup time by building the image locally and # running multiple test iterations. @@ -16,9 +16,9 @@ set -e # Configuration -IMAGE_NAME="retail-data-services:benchmark" -CONTAINER_PREFIX="retail-benchmark" -HEALTH_ENDPOINT="http://localhost:8080/retail_data_services/v1/health" +IMAGE_NAME="product-api:benchmark" +CONTAINER_PREFIX="product-api-benchmark" +HEALTH_ENDPOINT="http://localhost:8080/v1/health" DEFAULT_ITERATIONS=3 HEALTH_TIMEOUT=60 WAIT_BETWEEN_ITERATIONS=2 @@ -75,7 +75,7 @@ cleanup_container() { check_port_available() { local port=$1 - if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1 ; then + if lsof -Pi :"$port" -sTCP:LISTEN -t >/dev/null 2>&1 ; then return 1 else return 0 @@ -88,7 +88,7 @@ calculate_stats() { eval "local -a arr=(\"\${${array_name}[@]}\")" local count=${#arr[@]} - if [ $count -eq 0 ]; then + if [ "$count" -eq 0 ]; then echo "N/A" return fi @@ -128,11 +128,11 @@ calculate_stats() { # Main Benchmark Logic # ============================================================================ -print_header "Retail Data Services Startup Benchmark" +print_header "Product API Startup Benchmark" # Check if JAR exists -if [ ! -f "build/libs/retail-data-services.jar" ]; then - print_error "JAR file not found: build/libs/retail-data-services.jar" +if [ ! -f "build/libs/product-api.jar" ]; then + print_error "JAR file not found: build/libs/product-api.jar" print_info "Please run: ./gradlew clean build" exit 1 fi @@ -146,7 +146,7 @@ if ! check_port_available $PORT; then print_error "Both ports 8080 and 8081 are in use. Please free up a port." exit 1 fi - HEALTH_ENDPOINT="http://localhost:8081/retail_data_services/v1/health" + HEALTH_ENDPOINT="http://localhost:8081/v1/health" fi print_info "Using port: $PORT" @@ -172,7 +172,7 @@ print_info "Running $ITERATIONS benchmark iterations..." echo "" # Run benchmark iterations -for i in $(seq 1 $ITERATIONS); do +for i in $(seq 1 "$ITERATIONS"); do echo "----------------------------------------" echo "Iteration $i/$ITERATIONS" echo "----------------------------------------" @@ -250,7 +250,7 @@ for i in $(seq 1 $ITERATIONS); do cleanup_container "$container_name" # Wait between iterations - if [ $i -lt $ITERATIONS ]; then + if [ "$i" -lt "$ITERATIONS" ]; then sleep $WAIT_BETWEEN_ITERATIONS fi @@ -282,7 +282,7 @@ results_file="${script_dir}/benchmark-results-${timestamp}.txt" { echo "===========================================" - echo "Retail Data Services Startup Benchmark" + echo "Product API Startup Benchmark" echo "Timestamp: $(date)" echo "===========================================" echo "" diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/Main.java b/product-api/src/main/java/com/target/retail/product/Main.java similarity index 86% rename from retail-data-services/src/main/java/com/target/retail/data/services/Main.java rename to product-api/src/main/java/com/target/retail/product/Main.java index b4cdc48..4bacb0c 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/Main.java +++ b/product-api/src/main/java/com/target/retail/product/Main.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services; +package com.target.retail.product; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/config/CorsGlobalConfig.java b/product-api/src/main/java/com/target/retail/product/config/CorsGlobalConfig.java similarity index 95% rename from retail-data-services/src/main/java/com/target/retail/data/services/config/CorsGlobalConfig.java rename to product-api/src/main/java/com/target/retail/product/config/CorsGlobalConfig.java index 5867a4b..ffc1f4b 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/config/CorsGlobalConfig.java +++ b/product-api/src/main/java/com/target/retail/product/config/CorsGlobalConfig.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.config; +package com.target.retail.product.config; import org.springframework.context.annotation.Configuration; import org.springframework.boot.context.properties.EnableConfigurationProperties; diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/config/CorsProperties.java b/product-api/src/main/java/com/target/retail/product/config/CorsProperties.java similarity index 96% rename from retail-data-services/src/main/java/com/target/retail/data/services/config/CorsProperties.java rename to product-api/src/main/java/com/target/retail/product/config/CorsProperties.java index ad1a437..4406688 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/config/CorsProperties.java +++ b/product-api/src/main/java/com/target/retail/product/config/CorsProperties.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.config; +package com.target.retail.product.config; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/config/OpenApiConfig.java b/product-api/src/main/java/com/target/retail/product/config/OpenApiConfig.java similarity index 67% rename from retail-data-services/src/main/java/com/target/retail/data/services/config/OpenApiConfig.java rename to product-api/src/main/java/com/target/retail/product/config/OpenApiConfig.java index ae0d02f..3d3c452 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/config/OpenApiConfig.java +++ b/product-api/src/main/java/com/target/retail/product/config/OpenApiConfig.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.config; +package com.target.retail.product.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; @@ -12,9 +12,9 @@ public class OpenApiConfig { public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() - .title("Retail Data Services API") + .title("Product API") .version("1.0") - .description("API for managing retail data services.")); + .description("Read-only API for product catalog, pricing, and availability.")); } } diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/controller/AvailabilityController.java b/product-api/src/main/java/com/target/retail/product/controller/AvailabilityController.java similarity index 81% rename from retail-data-services/src/main/java/com/target/retail/data/services/controller/AvailabilityController.java rename to product-api/src/main/java/com/target/retail/product/controller/AvailabilityController.java index d1622ab..ccc5fb4 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/controller/AvailabilityController.java +++ b/product-api/src/main/java/com/target/retail/product/controller/AvailabilityController.java @@ -1,9 +1,10 @@ -package com.target.retail.data.services.controller; +package com.target.retail.product.controller; -import com.target.retail.data.services.dto.AvailabilityResponse; -import com.target.retail.data.services.model.ItemAvailability; -import com.target.retail.data.services.service.AvailabilityService; -import com.target.retail.data.services.service.behavior.Behaviors; +import com.target.retail.product.dto.AvailabilityResponse; +import com.target.retail.product.exception.AvailabilityNotFoundException; +import com.target.retail.product.model.ItemAvailability; +import com.target.retail.product.service.AvailabilityService; +import com.target.retail.product.service.behavior.Behaviors; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -18,6 +19,7 @@ import java.util.Optional; @RestController +@RequestMapping("/v1") public class AvailabilityController { private final AvailabilityService availabilityService; @@ -43,7 +45,7 @@ public ResponseEntity getAvailability(@PathVariable String return behaviors.getConfiguredBehavior().execute(() -> itemAvailability.map(it -> ResponseEntity.ok(new AvailabilityResponse(it))) - .orElseGet(() -> ResponseEntity.notFound().build()) + .orElseThrow(() -> new AvailabilityNotFoundException(id)) ); } diff --git a/product-api/src/main/java/com/target/retail/product/controller/GlobalExceptionHandler.java b/product-api/src/main/java/com/target/retail/product/controller/GlobalExceptionHandler.java new file mode 100644 index 0000000..122e322 --- /dev/null +++ b/product-api/src/main/java/com/target/retail/product/controller/GlobalExceptionHandler.java @@ -0,0 +1,40 @@ +package com.target.retail.product.controller; + +import com.target.retail.product.data.DataException; +import com.target.retail.product.dto.ErrorResponse; +import com.target.retail.product.exception.AvailabilityNotFoundException; +import com.target.retail.product.exception.InducedFailureException; +import com.target.retail.product.exception.ItemNotFoundException; +import com.target.retail.product.exception.PriceNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler({ItemNotFoundException.class, PriceNotFoundException.class, AvailabilityNotFoundException.class}) + public ResponseEntity handleNotFound(RuntimeException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage())); + } + + @ExceptionHandler(InducedFailureException.class) + public ResponseEntity handleInducedFailure(InducedFailureException ex) { + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) + .body(new ErrorResponse(HttpStatus.SERVICE_UNAVAILABLE.value(), ex.getMessage())); + } + + @ExceptionHandler(DataException.class) + public ResponseEntity handleDataException(DataException ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An internal data error occurred")); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred")); + } +} diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/controller/HealthController.java b/product-api/src/main/java/com/target/retail/product/controller/HealthController.java similarity index 91% rename from retail-data-services/src/main/java/com/target/retail/data/services/controller/HealthController.java rename to product-api/src/main/java/com/target/retail/product/controller/HealthController.java index ecbd2e4..88d2c94 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/controller/HealthController.java +++ b/product-api/src/main/java/com/target/retail/product/controller/HealthController.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.controller; +package com.target.retail.product.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController +@RequestMapping("/v1") public class HealthController { @Operation(summary = "Get health status", description = "Returns the health status of the application.") diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/controller/ItemController.java b/product-api/src/main/java/com/target/retail/product/controller/ItemController.java similarity index 86% rename from retail-data-services/src/main/java/com/target/retail/data/services/controller/ItemController.java rename to product-api/src/main/java/com/target/retail/product/controller/ItemController.java index 1137ef7..84466d0 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/controller/ItemController.java +++ b/product-api/src/main/java/com/target/retail/product/controller/ItemController.java @@ -1,10 +1,11 @@ -package com.target.retail.data.services.controller; +package com.target.retail.product.controller; -import com.target.retail.data.services.dto.ItemResponse; -import com.target.retail.data.services.dto.PaginatedResponse; -import com.target.retail.data.services.model.Item; -import com.target.retail.data.services.service.ItemService; -import com.target.retail.data.services.service.behavior.Behaviors; +import com.target.retail.product.dto.ItemResponse; +import com.target.retail.product.dto.PaginatedResponse; +import com.target.retail.product.exception.ItemNotFoundException; +import com.target.retail.product.model.Item; +import com.target.retail.product.service.ItemService; +import com.target.retail.product.service.behavior.Behaviors; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -18,6 +19,7 @@ import java.util.Optional; @RestController +@RequestMapping("/v1") public class ItemController { private final ItemService itemService; @@ -35,7 +37,9 @@ public ItemController(ItemService itemService, Behaviors behaviors) { @GetMapping("/items/{id}") public ResponseEntity getItem(@PathVariable String id) { Optional item = itemService.getItem(id); - return behaviors.getConfiguredBehavior().execute(() -> item.map(it -> ResponseEntity.ok(new ItemResponse(it))).orElseGet(() -> ResponseEntity.notFound().build())); + return behaviors.getConfiguredBehavior().execute(() -> item + .map(it -> ResponseEntity.ok(new ItemResponse(it))) + .orElseThrow(() -> new ItemNotFoundException(id))); } @Operation(summary = "Get all products with pagination", description = "Returns a paginated list of products with optional filtering.") diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/controller/PriceController.java b/product-api/src/main/java/com/target/retail/product/controller/PriceController.java similarity index 77% rename from retail-data-services/src/main/java/com/target/retail/data/services/controller/PriceController.java rename to product-api/src/main/java/com/target/retail/product/controller/PriceController.java index e88d79b..e9b93db 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/controller/PriceController.java +++ b/product-api/src/main/java/com/target/retail/product/controller/PriceController.java @@ -1,9 +1,10 @@ -package com.target.retail.data.services.controller; +package com.target.retail.product.controller; -import com.target.retail.data.services.dto.PriceResponse; -import com.target.retail.data.services.model.ItemPrice; -import com.target.retail.data.services.service.PriceService; -import com.target.retail.data.services.service.behavior.Behaviors; +import com.target.retail.product.dto.PriceResponse; +import com.target.retail.product.exception.PriceNotFoundException; +import com.target.retail.product.model.ItemPrice; +import com.target.retail.product.service.PriceService; +import com.target.retail.product.service.behavior.Behaviors; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -16,6 +17,7 @@ import java.util.Optional; @RestController +@RequestMapping("/v1") public class PriceController { private final PriceService priceService; @@ -32,7 +34,7 @@ public PriceController(PriceService priceService, Behaviors behaviors) { @GetMapping("/prices/{id}") public ResponseEntity getPrice(@PathVariable String id) { Optional itemPrice = priceService.getPrice(id); - return behaviors.getConfiguredBehavior().execute(() -> itemPrice.map(price -> ResponseEntity.ok(new PriceResponse(price))).orElseGet(() -> ResponseEntity.notFound().build())); + return behaviors.getConfiguredBehavior().execute(() -> itemPrice.map(price -> ResponseEntity.ok(new PriceResponse(price))).orElseThrow(() -> new PriceNotFoundException(id))); } } \ No newline at end of file diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/data/CsvData.java b/product-api/src/main/java/com/target/retail/product/data/CsvData.java similarity index 97% rename from retail-data-services/src/main/java/com/target/retail/data/services/data/CsvData.java rename to product-api/src/main/java/com/target/retail/product/data/CsvData.java index 958dbc5..371fedb 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/data/CsvData.java +++ b/product-api/src/main/java/com/target/retail/product/data/CsvData.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.data; +package com.target.retail.product.data; import com.fasterxml.jackson.databind.MappingIterator; import com.fasterxml.jackson.dataformat.csv.CsvMapper; diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/data/DataException.java b/product-api/src/main/java/com/target/retail/product/data/DataException.java similarity index 82% rename from retail-data-services/src/main/java/com/target/retail/data/services/data/DataException.java rename to product-api/src/main/java/com/target/retail/product/data/DataException.java index f266619..8d1c50d 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/data/DataException.java +++ b/product-api/src/main/java/com/target/retail/product/data/DataException.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.data; +package com.target.retail.product.data; public class DataException extends RuntimeException { diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/data/Identifiable.java b/product-api/src/main/java/com/target/retail/product/data/Identifiable.java similarity index 57% rename from retail-data-services/src/main/java/com/target/retail/data/services/data/Identifiable.java rename to product-api/src/main/java/com/target/retail/product/data/Identifiable.java index 0667bf7..cd2ae67 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/data/Identifiable.java +++ b/product-api/src/main/java/com/target/retail/product/data/Identifiable.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.data; +package com.target.retail.product.data; public interface Identifiable { public String getId(); diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/dto/AvailabilityResponse.java b/product-api/src/main/java/com/target/retail/product/dto/AvailabilityResponse.java similarity index 63% rename from retail-data-services/src/main/java/com/target/retail/data/services/dto/AvailabilityResponse.java rename to product-api/src/main/java/com/target/retail/product/dto/AvailabilityResponse.java index 521cb15..6e18db5 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/dto/AvailabilityResponse.java +++ b/product-api/src/main/java/com/target/retail/product/dto/AvailabilityResponse.java @@ -1,11 +1,11 @@ -package com.target.retail.data.services.dto; +package com.target.retail.product.dto; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.target.retail.data.services.model.ItemAvailability; +import com.target.retail.product.model.ItemAvailability; @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -public record AvailabilityResponse(String productId, Integer availableUnits, Integer limitedQuantityThreshold) { +public record AvailabilityResponse(String itemId, Integer availableUnits, Integer limitedQuantityThreshold) { public AvailabilityResponse(ItemAvailability itemAvailability) { this(itemAvailability.itemId(), itemAvailability.availableUnits(), itemAvailability.limitedQuantityThreshold()); diff --git a/product-api/src/main/java/com/target/retail/product/dto/ErrorResponse.java b/product-api/src/main/java/com/target/retail/product/dto/ErrorResponse.java new file mode 100644 index 0000000..ca6ed2f --- /dev/null +++ b/product-api/src/main/java/com/target/retail/product/dto/ErrorResponse.java @@ -0,0 +1,4 @@ +package com.target.retail.product.dto; + +public record ErrorResponse(int status, String message) { +} diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/dto/ItemResponse.java b/product-api/src/main/java/com/target/retail/product/dto/ItemResponse.java similarity index 77% rename from retail-data-services/src/main/java/com/target/retail/data/services/dto/ItemResponse.java rename to product-api/src/main/java/com/target/retail/product/dto/ItemResponse.java index f90b03d..2943446 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/dto/ItemResponse.java +++ b/product-api/src/main/java/com/target/retail/product/dto/ItemResponse.java @@ -1,8 +1,8 @@ -package com.target.retail.data.services.dto; +package com.target.retail.product.dto; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.target.retail.data.services.model.Item; +import com.target.retail.product.model.Item; @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public record ItemResponse(String itemId, String smallDescription, String longDescription, String category, @@ -16,9 +16,9 @@ public ItemResponse(Item item) { } @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - public record ImageData(String primaryImage, String alternateImage, String baseUrl) { + public record ImageData(String primary, String alternate, String baseUrl) { public ImageData(Item item) { - this(item.primaryImage(), item.alternateImage(), item.baseUrl()); + this(item.primary(), item.alternate(), item.baseUrl()); } } } \ No newline at end of file diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/dto/PaginatedResponse.java b/product-api/src/main/java/com/target/retail/product/dto/PaginatedResponse.java similarity index 93% rename from retail-data-services/src/main/java/com/target/retail/data/services/dto/PaginatedResponse.java rename to product-api/src/main/java/com/target/retail/product/dto/PaginatedResponse.java index 3e8e09d..46d6962 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/dto/PaginatedResponse.java +++ b/product-api/src/main/java/com/target/retail/product/dto/PaginatedResponse.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.dto; +package com.target.retail.product.dto; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; @@ -14,6 +14,6 @@ public PaginatedResponse(int currentPage, List items, int totalItems, int pag private static Integer calculateNextPage(int currentPage, int totalItems, int pageSize) { int totalPages = (totalItems + pageSize - 1) / pageSize; - return (currentPage + 1 < totalPages) ? currentPage + 1 : 0; + return (currentPage + 1 < totalPages) ? currentPage + 1 : null; } } \ No newline at end of file diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/dto/PriceResponse.java b/product-api/src/main/java/com/target/retail/product/dto/PriceResponse.java similarity index 63% rename from retail-data-services/src/main/java/com/target/retail/data/services/dto/PriceResponse.java rename to product-api/src/main/java/com/target/retail/product/dto/PriceResponse.java index 987a7e3..d5b7052 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/dto/PriceResponse.java +++ b/product-api/src/main/java/com/target/retail/product/dto/PriceResponse.java @@ -1,14 +1,14 @@ -package com.target.retail.data.services.dto; +package com.target.retail.product.dto; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.target.retail.data.services.model.ItemPrice; +import com.target.retail.product.model.ItemPrice; import java.math.BigDecimal; @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -public record PriceResponse(String productId, BigDecimal regularPrice, BigDecimal salePrice, String priceType) { +public record PriceResponse(String itemId, BigDecimal regular, BigDecimal sale, String priceType) { public PriceResponse(ItemPrice itemPrice) { this(itemPrice.itemId(), itemPrice.regularPrice(), itemPrice.salePrice(), itemPrice.type()); } diff --git a/product-api/src/main/java/com/target/retail/product/exception/AvailabilityNotFoundException.java b/product-api/src/main/java/com/target/retail/product/exception/AvailabilityNotFoundException.java new file mode 100644 index 0000000..e299df6 --- /dev/null +++ b/product-api/src/main/java/com/target/retail/product/exception/AvailabilityNotFoundException.java @@ -0,0 +1,7 @@ +package com.target.retail.product.exception; + +public class AvailabilityNotFoundException extends RuntimeException { + public AvailabilityNotFoundException(String id) { + super("Availability not found for item id " + id); + } +} diff --git a/product-api/src/main/java/com/target/retail/product/exception/InducedFailureException.java b/product-api/src/main/java/com/target/retail/product/exception/InducedFailureException.java new file mode 100644 index 0000000..bef1d83 --- /dev/null +++ b/product-api/src/main/java/com/target/retail/product/exception/InducedFailureException.java @@ -0,0 +1,7 @@ +package com.target.retail.product.exception; + +public class InducedFailureException extends RuntimeException { + public InducedFailureException(String message) { + super(message); + } +} diff --git a/product-api/src/main/java/com/target/retail/product/exception/ItemNotFoundException.java b/product-api/src/main/java/com/target/retail/product/exception/ItemNotFoundException.java new file mode 100644 index 0000000..80706d3 --- /dev/null +++ b/product-api/src/main/java/com/target/retail/product/exception/ItemNotFoundException.java @@ -0,0 +1,7 @@ +package com.target.retail.product.exception; + +public class ItemNotFoundException extends RuntimeException { + public ItemNotFoundException(String id) { + super("Item not found with id " + id); + } +} diff --git a/product-api/src/main/java/com/target/retail/product/exception/PriceNotFoundException.java b/product-api/src/main/java/com/target/retail/product/exception/PriceNotFoundException.java new file mode 100644 index 0000000..ce822d0 --- /dev/null +++ b/product-api/src/main/java/com/target/retail/product/exception/PriceNotFoundException.java @@ -0,0 +1,7 @@ +package com.target.retail.product.exception; + +public class PriceNotFoundException extends RuntimeException { + public PriceNotFoundException(String id) { + super("Price not found for item id " + id); + } +} diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/model/Item.java b/product-api/src/main/java/com/target/retail/product/model/Item.java similarity index 73% rename from retail-data-services/src/main/java/com/target/retail/data/services/model/Item.java rename to product-api/src/main/java/com/target/retail/product/model/Item.java index 8eec24e..96bcdab 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/model/Item.java +++ b/product-api/src/main/java/com/target/retail/product/model/Item.java @@ -1,17 +1,17 @@ -package com.target.retail.data.services.model; +package com.target.retail.product.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.target.retail.data.services.data.Identifiable; +import com.target.retail.product.data.Identifiable; -@JsonPropertyOrder({"itemId", "smallDescription", "longDescription", "category", "merchClass", "channelRestriction", "barcode", "brandName", "ageRestriction", "primaryImage", "alternateImage", "baseUrl"}) +@JsonPropertyOrder({"itemId", "smallDescription", "longDescription", "category", "merchClass", "channelRestriction", "barcode", "brandName", "ageRestriction", "primary", "alternate", "baseUrl"}) @JsonIgnoreProperties(ignoreUnknown = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public record Item(String itemId, String smallDescription, String longDescription, String category, Integer merchClass, String channelRestriction, String barcode, String brandName, Integer ageRestriction, - String primaryImage, String alternateImage, String baseUrl) implements Identifiable { + String primary, String alternate, String baseUrl) implements Identifiable { @Override public String getId() { diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/model/ItemAvailability.java b/product-api/src/main/java/com/target/retail/product/model/ItemAvailability.java similarity index 88% rename from retail-data-services/src/main/java/com/target/retail/data/services/model/ItemAvailability.java rename to product-api/src/main/java/com/target/retail/product/model/ItemAvailability.java index 76773a6..7792bb2 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/model/ItemAvailability.java +++ b/product-api/src/main/java/com/target/retail/product/model/ItemAvailability.java @@ -1,10 +1,10 @@ -package com.target.retail.data.services.model; +package com.target.retail.product.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.target.retail.data.services.data.Identifiable; +import com.target.retail.product.data.Identifiable; @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @JsonPropertyOrder({ "item_id", "availableUnits", "limitedQuantityThreshold" }) diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/model/ItemPrice.java b/product-api/src/main/java/com/target/retail/product/model/ItemPrice.java similarity index 76% rename from retail-data-services/src/main/java/com/target/retail/data/services/model/ItemPrice.java rename to product-api/src/main/java/com/target/retail/product/model/ItemPrice.java index 38fb1d4..c5ca07c 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/model/ItemPrice.java +++ b/product-api/src/main/java/com/target/retail/product/model/ItemPrice.java @@ -1,14 +1,14 @@ -package com.target.retail.data.services.model; +package com.target.retail.product.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.target.retail.data.services.data.Identifiable; +import com.target.retail.product.data.Identifiable; import java.math.BigDecimal; @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -@JsonPropertyOrder({ "itemId", "regular_price", "sale_price", "type" }) +@JsonPropertyOrder({ "itemId", "regularPrice", "salePrice", "type" }) @JsonIgnoreProperties(ignoreUnknown = true) public record ItemPrice(String itemId, BigDecimal regularPrice, BigDecimal salePrice, String type) implements Identifiable { diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/service/AvailabilityService.java b/product-api/src/main/java/com/target/retail/product/service/AvailabilityService.java similarity index 91% rename from retail-data-services/src/main/java/com/target/retail/data/services/service/AvailabilityService.java rename to product-api/src/main/java/com/target/retail/product/service/AvailabilityService.java index e940a2d..8fb070f 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/service/AvailabilityService.java +++ b/product-api/src/main/java/com/target/retail/product/service/AvailabilityService.java @@ -1,7 +1,7 @@ -package com.target.retail.data.services.service; +package com.target.retail.product.service; -import com.target.retail.data.services.data.CsvData; -import com.target.retail.data.services.model.ItemAvailability; +import com.target.retail.product.data.CsvData; +import com.target.retail.product.model.ItemAvailability; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/service/ItemService.java b/product-api/src/main/java/com/target/retail/product/service/ItemService.java similarity index 94% rename from retail-data-services/src/main/java/com/target/retail/data/services/service/ItemService.java rename to product-api/src/main/java/com/target/retail/product/service/ItemService.java index d3b3fa7..242bc0b 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/service/ItemService.java +++ b/product-api/src/main/java/com/target/retail/product/service/ItemService.java @@ -1,7 +1,7 @@ -package com.target.retail.data.services.service; +package com.target.retail.product.service; -import com.target.retail.data.services.data.CsvData; -import com.target.retail.data.services.model.Item; +import com.target.retail.product.data.CsvData; +import com.target.retail.product.model.Item; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/service/PriceService.java b/product-api/src/main/java/com/target/retail/product/service/PriceService.java similarity index 91% rename from retail-data-services/src/main/java/com/target/retail/data/services/service/PriceService.java rename to product-api/src/main/java/com/target/retail/product/service/PriceService.java index 0af7b7c..1671b55 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/service/PriceService.java +++ b/product-api/src/main/java/com/target/retail/product/service/PriceService.java @@ -1,7 +1,7 @@ -package com.target.retail.data.services.service; +package com.target.retail.product.service; -import com.target.retail.data.services.data.CsvData; -import com.target.retail.data.services.model.ItemPrice; +import com.target.retail.product.data.CsvData; +import com.target.retail.product.model.ItemPrice; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/service/behavior/BehaviorType.java b/product-api/src/main/java/com/target/retail/product/service/behavior/BehaviorType.java similarity index 57% rename from retail-data-services/src/main/java/com/target/retail/data/services/service/behavior/BehaviorType.java rename to product-api/src/main/java/com/target/retail/product/service/behavior/BehaviorType.java index ebc393b..8732bfa 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/service/behavior/BehaviorType.java +++ b/product-api/src/main/java/com/target/retail/product/service/behavior/BehaviorType.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.service.behavior; +package com.target.retail.product.service.behavior; public enum BehaviorType { NORMAL, diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/service/behavior/Behaviors.java b/product-api/src/main/java/com/target/retail/product/service/behavior/Behaviors.java similarity index 81% rename from retail-data-services/src/main/java/com/target/retail/data/services/service/behavior/Behaviors.java rename to product-api/src/main/java/com/target/retail/product/service/behavior/Behaviors.java index c95112e..278086f 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/service/behavior/Behaviors.java +++ b/product-api/src/main/java/com/target/retail/product/service/behavior/Behaviors.java @@ -1,5 +1,6 @@ -package com.target.retail.data.services.service.behavior; +package com.target.retail.product.service.behavior; +import com.target.retail.product.exception.InducedFailureException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -24,7 +25,7 @@ public class Behaviors { @Value("${DEFAULT_BEHAVIOR:NORMAL}") private BehaviorType configuredDefaultBehavior; - private final Map behaviorMap; + private final Map behaviorMap; private Random random; @@ -32,9 +33,9 @@ public Behaviors() { random = new Random(); behaviorMap = new HashMap<>(); - behaviorMap.put(BehaviorType.NORMAL.name(), this::callWithNoInducedBehavior); - behaviorMap.put(BehaviorType.SLOW_RESPONSE.name(), this::callWithSlowResponse); - behaviorMap.put(BehaviorType.RANDOM_FAILURES.name(), this::callWithRandomFailures); + behaviorMap.put(BehaviorType.NORMAL, this::callWithNoInducedBehavior); + behaviorMap.put(BehaviorType.SLOW_RESPONSE, this::callWithSlowResponse); + behaviorMap.put(BehaviorType.RANDOM_FAILURES, this::callWithRandomFailures); } public Behaviors(BehaviorType defaultBehavior, double failingBehaviorFailureRate, int slowResponseBehaviorMinimumDelay, int slowResponseBehaviorMaximumDelay) { @@ -56,7 +57,7 @@ public InducedBehavior getConfiguredBehavior() { } public Optional getBehavior(BehaviorType type) { - return Optional.ofNullable(behaviorMap.get(type.name())); + return Optional.ofNullable(behaviorMap.get(type)); } private T callWithSlowResponse(Supplier supplier) { @@ -76,7 +77,7 @@ private T callWithNoInducedBehavior(Supplier supplier) { private T callWithRandomFailures(Supplier supplier) { if(random.nextDouble() < failingBehaviorFailureRate) { - throw new RuntimeException("Failure to call the service. Please try later"); + throw new InducedFailureException("Failure to call the service. Please try later"); } return supplier.get(); } diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/service/behavior/InducedBehavior.java b/product-api/src/main/java/com/target/retail/product/service/behavior/InducedBehavior.java similarity index 70% rename from retail-data-services/src/main/java/com/target/retail/data/services/service/behavior/InducedBehavior.java rename to product-api/src/main/java/com/target/retail/product/service/behavior/InducedBehavior.java index 1a1acbf..09c063a 100644 --- a/retail-data-services/src/main/java/com/target/retail/data/services/service/behavior/InducedBehavior.java +++ b/product-api/src/main/java/com/target/retail/product/service/behavior/InducedBehavior.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.service.behavior; +package com.target.retail.product.service.behavior; import java.util.function.Supplier; diff --git a/retail-data-services/src/main/resources/application.yml b/product-api/src/main/resources/application.yml similarity index 76% rename from retail-data-services/src/main/resources/application.yml rename to product-api/src/main/resources/application.yml index 8c0bcf1..d2131fd 100644 --- a/retail-data-services/src/main/resources/application.yml +++ b/product-api/src/main/resources/application.yml @@ -6,8 +6,6 @@ spring: server: port: 8080 - servlet: - context-path: /retail_data_services/v1/ management: endpoints: @@ -26,8 +24,6 @@ springdoc: path: api-docs swagger-ui: enabled: true - config-url: /retail_data_services/v1/api-docs/swagger-config - url: /retail_data_services/v1/api-docs price: data-file: /data/prices.csv @@ -45,4 +41,4 @@ behaviors: failure-rate: 0.5 slow-response: min-delay-ms: 1000 - max-delay-ms: 5000 \ No newline at end of file + max-delay-ms: 5000 diff --git a/retail-data-services/src/main/resources/availability.csv b/product-api/src/main/resources/availability.csv similarity index 100% rename from retail-data-services/src/main/resources/availability.csv rename to product-api/src/main/resources/availability.csv diff --git a/retail-data-services/src/main/resources/items.csv b/product-api/src/main/resources/items.csv similarity index 100% rename from retail-data-services/src/main/resources/items.csv rename to product-api/src/main/resources/items.csv diff --git a/retail-data-services/src/main/resources/prices.csv b/product-api/src/main/resources/prices.csv similarity index 100% rename from retail-data-services/src/main/resources/prices.csv rename to product-api/src/main/resources/prices.csv diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/config/CorsIntegrationTest.java b/product-api/src/test/java/com/target/retail/product/config/CorsIntegrationTest.java similarity index 96% rename from retail-data-services/src/test/java/com/target/retail/data/services/config/CorsIntegrationTest.java rename to product-api/src/test/java/com/target/retail/product/config/CorsIntegrationTest.java index f844420..32d2a5d 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/config/CorsIntegrationTest.java +++ b/product-api/src/test/java/com/target/retail/product/config/CorsIntegrationTest.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.config; +package com.target.retail.product.config; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/config/CorsPropertiesTest.java b/product-api/src/test/java/com/target/retail/product/config/CorsPropertiesTest.java similarity index 94% rename from retail-data-services/src/test/java/com/target/retail/data/services/config/CorsPropertiesTest.java rename to product-api/src/test/java/com/target/retail/product/config/CorsPropertiesTest.java index 33fc9aa..beb6af8 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/config/CorsPropertiesTest.java +++ b/product-api/src/test/java/com/target/retail/product/config/CorsPropertiesTest.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.config; +package com.target.retail.product.config; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/controllers/AvailabilityControllerTest.java b/product-api/src/test/java/com/target/retail/product/controllers/AvailabilityControllerTest.java similarity index 73% rename from retail-data-services/src/test/java/com/target/retail/data/services/controllers/AvailabilityControllerTest.java rename to product-api/src/test/java/com/target/retail/product/controllers/AvailabilityControllerTest.java index 0b0c5c4..e004876 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/controllers/AvailabilityControllerTest.java +++ b/product-api/src/test/java/com/target/retail/product/controllers/AvailabilityControllerTest.java @@ -1,11 +1,12 @@ -package com.target.retail.data.services.controllers; +package com.target.retail.product.controllers; -import com.target.retail.data.services.controller.AvailabilityController; -import com.target.retail.data.services.dto.AvailabilityResponse; -import com.target.retail.data.services.model.ItemAvailability; -import com.target.retail.data.services.service.AvailabilityService; -import com.target.retail.data.services.service.behavior.Behaviors; -import com.target.retail.data.services.service.behavior.InducedBehavior; +import com.target.retail.product.controller.AvailabilityController; +import com.target.retail.product.dto.AvailabilityResponse; +import com.target.retail.product.exception.AvailabilityNotFoundException; +import com.target.retail.product.model.ItemAvailability; +import com.target.retail.product.service.AvailabilityService; +import com.target.retail.product.service.behavior.Behaviors; +import com.target.retail.product.service.behavior.InducedBehavior; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.ResponseEntity; @@ -16,6 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -47,7 +49,7 @@ void shouldReturnAvailabilityResponse() { ResponseEntity response = availabilityController.getAvailability(productId); - assertEquals(productId, Objects.requireNonNull(response.getBody()).productId(), "Product ID does not match."); + assertEquals(productId, Objects.requireNonNull(response.getBody()).itemId(), "Product ID does not match."); AvailabilityResponse responseBody = response.getBody(); assertNotNull(responseBody); @@ -60,8 +62,7 @@ void shouldReturnAvailabilityResponse() { void shouldReturn404WhenAvailabilityNotFound() { String productId = "11111"; when(availabilityService.getItemAvailability(productId)).thenReturn(Optional.empty()); - ResponseEntity response = availabilityController.getAvailability(productId); - assertEquals(404, response.getStatusCode().value(), "Unexpected HTTP Status " + response.getStatusCode()); + assertThrows(AvailabilityNotFoundException.class, () -> availabilityController.getAvailability(productId)); } } diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/controllers/HealthControllerTest.java b/product-api/src/test/java/com/target/retail/product/controllers/HealthControllerTest.java similarity index 79% rename from retail-data-services/src/test/java/com/target/retail/data/services/controllers/HealthControllerTest.java rename to product-api/src/test/java/com/target/retail/product/controllers/HealthControllerTest.java index a9510b0..b09a3dc 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/controllers/HealthControllerTest.java +++ b/product-api/src/test/java/com/target/retail/product/controllers/HealthControllerTest.java @@ -1,7 +1,7 @@ -package com.target.retail.data.services.controllers; +package com.target.retail.product.controllers; -import com.target.retail.data.services.controller.HealthController; +import com.target.retail.product.controller.HealthController; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/controllers/ItemControllerTest.java b/product-api/src/test/java/com/target/retail/product/controllers/ItemControllerTest.java similarity index 88% rename from retail-data-services/src/test/java/com/target/retail/data/services/controllers/ItemControllerTest.java rename to product-api/src/test/java/com/target/retail/product/controllers/ItemControllerTest.java index 6e96b4a..794f992 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/controllers/ItemControllerTest.java +++ b/product-api/src/test/java/com/target/retail/product/controllers/ItemControllerTest.java @@ -1,13 +1,14 @@ -package com.target.retail.data.services.controllers; - -import com.target.retail.data.services.controller.ItemController; -import com.target.retail.data.services.dto.ItemResponse; -import com.target.retail.data.services.dto.ItemResponse.ImageData; -import com.target.retail.data.services.dto.PaginatedResponse; -import com.target.retail.data.services.model.Item; -import com.target.retail.data.services.service.ItemService; -import com.target.retail.data.services.service.behavior.Behaviors; -import com.target.retail.data.services.service.behavior.InducedBehavior; +package com.target.retail.product.controllers; + +import com.target.retail.product.controller.ItemController; +import com.target.retail.product.dto.ItemResponse; +import com.target.retail.product.dto.ItemResponse.ImageData; +import com.target.retail.product.dto.PaginatedResponse; +import com.target.retail.product.exception.ItemNotFoundException; +import com.target.retail.product.model.Item; +import com.target.retail.product.service.ItemService; +import com.target.retail.product.service.behavior.Behaviors; +import com.target.retail.product.service.behavior.InducedBehavior; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.ResponseEntity; @@ -41,7 +42,7 @@ public T execute(Supplier supplier) { @Test public void testGetItem_Found() { Item item = new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, - "primary_image", "alternate_image", "http://target.com"); + "primary", "alternate", "http://target.com"); when(itemService.getItem("901234")).thenReturn(Optional.of(item)); ResponseEntity response = itemController.getItem("901234"); @@ -58,16 +59,13 @@ public void testGetItem_Found() { @Test public void testGetItem_NotFound() { when(itemService.getItem("999999")).thenReturn(Optional.empty()); - - ResponseEntity response = itemController.getItem("999999"); - - assertEquals(404, response.getStatusCode().value()); + assertThrows(ItemNotFoundException.class, () -> itemController.getItem("999999")); } @Test public void testGetItem_ImageBlock() { Item item = new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, - "primary_image", "alternate_image", "http://target.com"); + "primary", "alternate", "http://target.com"); when(itemService.getItem("901234")).thenReturn(Optional.of(item)); ResponseEntity response = itemController.getItem("901234"); @@ -78,8 +76,8 @@ public void testGetItem_ImageBlock() { assertNotNull(responseBody); ImageData imageData = responseBody.imageData(); - assertEquals("primary_image", imageData.primaryImage()); - assertEquals("alternate_image", imageData.alternateImage()); + assertEquals("primary", imageData.primary()); + assertEquals("alternate", imageData.alternate()); assertEquals("http://target.com", imageData.baseUrl()); } @@ -87,9 +85,9 @@ public void testGetItem_ImageBlock() { public void getAllItems_ReturnsPaginatedResponse_WhenValidParameters() { List items = List.of( new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, - "primary_image", "alternate_image", "http://target.com"), + "primary", "alternate", "http://target.com"), new Item("901235", "Small Desc 2", "Long Desc 2", "Category 2", 13, "ONLINE", "Barcode 2", "Brand 2", - 21, "primary_image_2", "alternate_image_2", "http://target.com/2")); + 21, "primary_2", "alternate_2", "http://target.com/2")); when(itemService.getAllItems(0, 2, null)).thenReturn(items); when(itemService.getItemCount(null)).thenReturn(10); @@ -128,7 +126,7 @@ public void getAllItems_ReturnsEmptyList_WhenNoItemsAvailable() { assertEquals(0, responseBody.currentPage()); assertTrue(responseBody.items().isEmpty()); - assertEquals(0, responseBody.nextPage()); + assertNull(responseBody.nextPage()); } @Test @@ -176,9 +174,9 @@ public void getAllItems_WithSmallDescriptionFilter_CaseInsensitive() { public void getAllItems_WithEmptySmallDescriptionFilter_ReturnsAllItems() { List allItems = List.of( new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, - "primary_image", "alternate_image", "http://target.com"), + "primary", "alternate", "http://target.com"), new Item("901235", "Small Desc 2", "Long Desc 2", "Category 2", 13, "ONLINE", "Barcode 2", "Brand 2", - 21, "primary_image_2", "alternate_image_2", "http://target.com/2")); + 21, "primary_2", "alternate_2", "http://target.com/2")); when(itemService.getAllItems(0, 2, "")).thenReturn(allItems); when(itemService.getItemCount("")).thenReturn(10); @@ -196,7 +194,7 @@ public void getAllItems_WithEmptySmallDescriptionFilter_ReturnsAllItems() { public void getAllItems_WithWhitespaceSmallDescriptionFilter_ReturnsAllItems() { List allItems = List.of( new Item("901234", "Test Item", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, - "primary_image", "alternate_image", "http://target.com")); + "primary", "alternate", "http://target.com")); when(itemService.getAllItems(0, 10, " ")).thenReturn(allItems); when(itemService.getItemCount(" ")).thenReturn(1); @@ -224,7 +222,7 @@ public void getAllItems_WithSmallDescriptionFilter_NoMatches_ReturnsEmptyList() assertTrue(responseBody.items().isEmpty()); assertEquals(0, responseBody.currentPage()); - assertEquals(0, responseBody.nextPage()); + assertNull(responseBody.nextPage()); } @Test diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/controllers/PriceControllerTest.java b/product-api/src/test/java/com/target/retail/product/controllers/PriceControllerTest.java similarity index 67% rename from retail-data-services/src/test/java/com/target/retail/data/services/controllers/PriceControllerTest.java rename to product-api/src/test/java/com/target/retail/product/controllers/PriceControllerTest.java index 587c7ea..635a4c1 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/controllers/PriceControllerTest.java +++ b/product-api/src/test/java/com/target/retail/product/controllers/PriceControllerTest.java @@ -1,11 +1,12 @@ -package com.target.retail.data.services.controllers; +package com.target.retail.product.controllers; -import com.target.retail.data.services.dto.PriceResponse; -import com.target.retail.data.services.model.ItemPrice; -import com.target.retail.data.services.service.PriceService; -import com.target.retail.data.services.controller.PriceController; -import com.target.retail.data.services.service.behavior.Behaviors; -import com.target.retail.data.services.service.behavior.InducedBehavior; +import com.target.retail.product.dto.PriceResponse; +import com.target.retail.product.exception.PriceNotFoundException; +import com.target.retail.product.model.ItemPrice; +import com.target.retail.product.service.PriceService; +import com.target.retail.product.controller.PriceController; +import com.target.retail.product.service.behavior.Behaviors; +import com.target.retail.product.service.behavior.InducedBehavior; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.ResponseEntity; @@ -17,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -52,13 +54,20 @@ void shouldReturnPriceResponse() { ResponseEntity response = priceController.getPrice(productId); - assertEquals(productId, Objects.requireNonNull(response.getBody()).productId(), "Product ID does not match."); + assertEquals(productId, Objects.requireNonNull(response.getBody()).itemId(), "Product ID does not match."); PriceResponse responseBody = response.getBody(); assertNotNull(responseBody); - assertEquals(BigDecimal.valueOf(19.99), responseBody.regularPrice(), "Price does not match."); + assertEquals(BigDecimal.valueOf(19.99), responseBody.regular(), "Price does not match."); assertEquals("REGULAR", responseBody.priceType(), "Price type does not match."); } + @Test + void shouldThrowPriceNotFoundException_WhenPriceNotFound() { + String productId = "99999"; + when(priceService.getPrice(productId)).thenReturn(Optional.empty()); + assertThrows(PriceNotFoundException.class, () -> priceController.getPrice(productId)); + } + } diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/data/CsvDataTest.java b/product-api/src/test/java/com/target/retail/product/data/CsvDataTest.java similarity index 99% rename from retail-data-services/src/test/java/com/target/retail/data/services/data/CsvDataTest.java rename to product-api/src/test/java/com/target/retail/product/data/CsvDataTest.java index eb186da..77e05f9 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/data/CsvDataTest.java +++ b/product-api/src/test/java/com/target/retail/product/data/CsvDataTest.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.data; +package com.target.retail.product.data; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.PropertyNamingStrategies; diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/integration/AvailabilityIntegrationTest.java b/product-api/src/test/java/com/target/retail/product/integration/AvailabilityIntegrationTest.java similarity index 81% rename from retail-data-services/src/test/java/com/target/retail/data/services/integration/AvailabilityIntegrationTest.java rename to product-api/src/test/java/com/target/retail/product/integration/AvailabilityIntegrationTest.java index 9af6000..e373427 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/integration/AvailabilityIntegrationTest.java +++ b/product-api/src/test/java/com/target/retail/product/integration/AvailabilityIntegrationTest.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.integration; +package com.target.retail.product.integration; import org.junit.jupiter.api.Test; @@ -11,7 +11,7 @@ public class AvailabilityIntegrationTest extends BaseIntegrationTest { public void testGetAvailability() throws Exception { getResponse("/availability/" + testProductId) .andExpect(status().isOk()) - .andExpect(jsonPath("$.product_id").value(equalTo(testProductId))); + .andExpect(jsonPath("$.item_id").value(equalTo(testProductId))); } @Test diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/integration/BaseIntegrationTest.java b/product-api/src/test/java/com/target/retail/product/integration/BaseIntegrationTest.java similarity index 89% rename from retail-data-services/src/test/java/com/target/retail/data/services/integration/BaseIntegrationTest.java rename to product-api/src/test/java/com/target/retail/product/integration/BaseIntegrationTest.java index 35a4ef1..5b2df35 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/integration/BaseIntegrationTest.java +++ b/product-api/src/test/java/com/target/retail/product/integration/BaseIntegrationTest.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.integration; +package com.target.retail.product.integration; import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; @@ -21,6 +21,6 @@ public class BaseIntegrationTest { protected String invalidProductId = "999999999"; public ResultActions getResponse(String url) throws Exception { - return mockMvc.perform(get(url)); + return mockMvc.perform(get("/v1" + url)); } } diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/integration/HealthIntegrationTest.java b/product-api/src/test/java/com/target/retail/product/integration/HealthIntegrationTest.java similarity index 80% rename from retail-data-services/src/test/java/com/target/retail/data/services/integration/HealthIntegrationTest.java rename to product-api/src/test/java/com/target/retail/product/integration/HealthIntegrationTest.java index 02459f8..63dfe97 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/integration/HealthIntegrationTest.java +++ b/product-api/src/test/java/com/target/retail/product/integration/HealthIntegrationTest.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.integration; +package com.target.retail.product.integration; import com.fasterxml.jackson.core.JsonProcessingException; import io.swagger.v3.core.util.Json; @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -21,7 +22,7 @@ public void testHealthCheck() throws Exception { @Test public void testActuatorHealth() throws Exception { - getResponse("/actuator/health") + mockMvc.perform(get("/actuator/health")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("UP")); } @@ -29,7 +30,7 @@ public void testActuatorHealth() throws Exception { @Test @Disabled("Unable to get this test to work even though the endpoint is working") public void testOpenApiSpec() throws Exception { - String specBody = getResponse("/api-docs") + String specBody = mockMvc.perform(get("/api-docs")) .andExpect(status().isOk()) .andReturn() .getResponse() @@ -37,6 +38,6 @@ public void testOpenApiSpec() throws Exception { OpenAPI spec = Json.mapper().readValue(specBody, OpenAPI.class); assertEquals(SpecVersion.V30, spec.getSpecVersion()); - assertEquals("Retail Data Services API", spec.getInfo().getTitle()); + assertEquals("Product API", spec.getInfo().getTitle()); } } diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/integration/ItemIntegrationTest.java b/product-api/src/test/java/com/target/retail/product/integration/ItemIntegrationTest.java similarity index 96% rename from retail-data-services/src/test/java/com/target/retail/data/services/integration/ItemIntegrationTest.java rename to product-api/src/test/java/com/target/retail/product/integration/ItemIntegrationTest.java index 9c14810..a2de86f 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/integration/ItemIntegrationTest.java +++ b/product-api/src/test/java/com/target/retail/product/integration/ItemIntegrationTest.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.integration; +package com.target.retail.product.integration; import org.junit.jupiter.api.Test; diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/integration/PriceIntegrationTest.java b/product-api/src/test/java/com/target/retail/product/integration/PriceIntegrationTest.java similarity index 80% rename from retail-data-services/src/test/java/com/target/retail/data/services/integration/PriceIntegrationTest.java rename to product-api/src/test/java/com/target/retail/product/integration/PriceIntegrationTest.java index b2b26d1..3f9f15d 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/integration/PriceIntegrationTest.java +++ b/product-api/src/test/java/com/target/retail/product/integration/PriceIntegrationTest.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.integration; +package com.target.retail.product.integration; import org.junit.jupiter.api.Test; @@ -11,7 +11,7 @@ public class PriceIntegrationTest extends BaseIntegrationTest { public void testGetPrice() throws Exception { getResponse("/prices/" + testProductId) .andExpect(status().isOk()) - .andExpect(jsonPath("$.product_id").value(equalTo(testProductId))); + .andExpect(jsonPath("$.item_id").value(equalTo(testProductId))); } @Test diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/service/AvailabilityServiceTest.java b/product-api/src/test/java/com/target/retail/product/service/AvailabilityServiceTest.java similarity index 92% rename from retail-data-services/src/test/java/com/target/retail/data/services/service/AvailabilityServiceTest.java rename to product-api/src/test/java/com/target/retail/product/service/AvailabilityServiceTest.java index 463584e..cd004c8 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/service/AvailabilityServiceTest.java +++ b/product-api/src/test/java/com/target/retail/product/service/AvailabilityServiceTest.java @@ -1,7 +1,7 @@ -package com.target.retail.data.services.service; +package com.target.retail.product.service; -import com.target.retail.data.services.data.CsvData; -import com.target.retail.data.services.model.ItemAvailability; +import com.target.retail.product.data.CsvData; +import com.target.retail.product.model.ItemAvailability; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/service/ItemServiceTest.java b/product-api/src/test/java/com/target/retail/product/service/ItemServiceTest.java similarity index 78% rename from retail-data-services/src/test/java/com/target/retail/data/services/service/ItemServiceTest.java rename to product-api/src/test/java/com/target/retail/product/service/ItemServiceTest.java index c5dcdd7..c8eccba 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/service/ItemServiceTest.java +++ b/product-api/src/test/java/com/target/retail/product/service/ItemServiceTest.java @@ -1,7 +1,7 @@ -package com.target.retail.data.services.service; +package com.target.retail.product.service; -import com.target.retail.data.services.data.CsvData; -import com.target.retail.data.services.model.Item; +import com.target.retail.product.data.CsvData; +import com.target.retail.product.model.Item; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -32,7 +32,7 @@ public void setUp() { @Test public void testGetItem_Found() { - Item item = new Item("901234", "Small Desc", "Long Desc", "Category", 12 ,"IN-STORE", "Barcode", "Brand", 18, "primary_image", "alternate_image", "http://target.com"); + Item item = new Item("901234", "Small Desc", "Long Desc", "Category", 12 ,"IN-STORE", "Barcode", "Brand", 18, "primary", "alternate", "http://target.com"); when(itemData.getById("901234")).thenReturn(Optional.of(item)); Optional response = itemService.getItem("901234"); @@ -59,21 +59,21 @@ public void testGetItem_NotFound() { @Test public void testGetItem_ImageDetails() { - Item item = new Item("901234", "Small Desc", "Long Desc", "Category", 12 ,"IN-STORE", "Barcode", "Brand", 18, "primary_image", "alternate_image", "http://target.com"); + Item item = new Item("901234", "Small Desc", "Long Desc", "Category", 12 ,"IN-STORE", "Barcode", "Brand", 18, "primary", "alternate", "http://target.com"); when(itemData.getById("901234")).thenReturn(Optional.of(item)); Optional response = itemService.getItem("901234"); assertTrue(response.isPresent()); - assertEquals("alternate_image", response.get().alternateImage()); - assertEquals("primary_image", response.get().primaryImage()); + assertEquals("alternate", response.get().alternate()); + assertEquals("primary", response.get().primary()); assertEquals("http://target.com", response.get().baseUrl()); } @Test public void getItemCount_ReturnsPaginatedList_WhenValidPageAndSize() { List items = List.of( - new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, "primary_image", "alternate_image", "http://target.com"), - new Item("901235", "Small Desc 2", "Long Desc 2", "Category 2", 13, "ONLINE", "Barcode 2", "Brand 2", 21, "primary_image_2", "alternate_image_2", "http://target.com/2") + new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, "primary", "alternate", "http://target.com"), + new Item("901235", "Small Desc 2", "Long Desc 2", "Category 2", 13, "ONLINE", "Barcode 2", "Brand 2", 21, "primary_2", "alternate_2", "http://target.com/2") ); when(itemData.getAll()).thenReturn(items); @@ -86,7 +86,7 @@ public void getItemCount_ReturnsPaginatedList_WhenValidPageAndSize() { @Test public void getItemCount_ReturnsEmptyList_WhenPageOutOfBounds() { List items = List.of( - new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, "primary_image", "alternate_image", "http://target.com") + new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, "primary", "alternate", "http://target.com") ); when(itemData.getAll()).thenReturn(items); @@ -98,7 +98,7 @@ public void getItemCount_ReturnsEmptyList_WhenPageOutOfBounds() { @Test public void getItemCount_ReturnsEmptyList_WhenSizeIsZero() { List items = List.of( - new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, "primary_image", "alternate_image", "http://target.com") + new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, "primary", "alternate", "http://target.com") ); when(itemData.getAll()).thenReturn(items); @@ -110,8 +110,8 @@ public void getItemCount_ReturnsEmptyList_WhenSizeIsZero() { @Test public void getItemCount_ReturnsTotalItemCount() { List items = List.of( - new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, "primary_image", "alternate_image", "http://target.com"), - new Item("901235", "Small Desc 2", "Long Desc 2", "Category 2", 13, "ONLINE", "Barcode 2", "Brand 2", 21, "primary_image_2", "alternate_image_2", "http://target.com/2") + new Item("901234", "Small Desc", "Long Desc", "Category", 12, "IN-STORE", "Barcode", "Brand", 18, "primary", "alternate", "http://target.com"), + new Item("901235", "Small Desc 2", "Long Desc 2", "Category 2", 13, "ONLINE", "Barcode 2", "Brand 2", 21, "primary_2", "alternate_2", "http://target.com/2") ); when(itemData.getCount()).thenReturn(items.size()); diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/service/PriceServiceTest.java b/product-api/src/test/java/com/target/retail/product/service/PriceServiceTest.java similarity index 80% rename from retail-data-services/src/test/java/com/target/retail/data/services/service/PriceServiceTest.java rename to product-api/src/test/java/com/target/retail/product/service/PriceServiceTest.java index b96c74f..6df8ba2 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/service/PriceServiceTest.java +++ b/product-api/src/test/java/com/target/retail/product/service/PriceServiceTest.java @@ -1,8 +1,8 @@ -package com.target.retail.data.services.service; +package com.target.retail.product.service; -import com.target.retail.data.services.data.CsvData; -import com.target.retail.data.services.dto.PriceResponse; -import com.target.retail.data.services.model.ItemPrice; +import com.target.retail.product.data.CsvData; +import com.target.retail.product.dto.PriceResponse; +import com.target.retail.product.model.ItemPrice; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -46,8 +46,8 @@ void shouldReturnPriceResponse() { Optional itemPrice = priceService.getPrice(productId); - assertEquals(expectedResponse.productId(), itemPrice.get().itemId(), "Product ID does not match."); - assertEquals(expectedResponse.regularPrice(), itemPrice.get().regularPrice(), "Price does not match."); + assertEquals(expectedResponse.itemId(), itemPrice.get().itemId(), "Product ID does not match."); + assertEquals(expectedResponse.regular(), itemPrice.get().regularPrice(), "Price does not match."); assertEquals(expectedResponse.priceType(), itemPrice.get().type(), "Price type does not match."); } diff --git a/retail-data-services/src/test/java/com/target/retail/data/services/service/behavior/BehaviorsTest.java b/product-api/src/test/java/com/target/retail/product/service/behavior/BehaviorsTest.java similarity index 98% rename from retail-data-services/src/test/java/com/target/retail/data/services/service/behavior/BehaviorsTest.java rename to product-api/src/test/java/com/target/retail/product/service/behavior/BehaviorsTest.java index cb9e52f..1b6672c 100644 --- a/retail-data-services/src/test/java/com/target/retail/data/services/service/behavior/BehaviorsTest.java +++ b/product-api/src/test/java/com/target/retail/product/service/behavior/BehaviorsTest.java @@ -1,4 +1,4 @@ -package com.target.retail.data.services.service.behavior; +package com.target.retail.product.service.behavior; import org.junit.jupiter.api.Test; diff --git a/retail-data-services/src/test/resources/application.yml b/product-api/src/test/resources/application.yml similarity index 100% rename from retail-data-services/src/test/resources/application.yml rename to product-api/src/test/resources/application.yml diff --git a/retail-data-services/src/main/java/com/target/retail/data/services/controller/CustomErrorController.java b/retail-data-services/src/main/java/com/target/retail/data/services/controller/CustomErrorController.java deleted file mode 100644 index c4d14b2..0000000 --- a/retail-data-services/src/main/java/com/target/retail/data/services/controller/CustomErrorController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.target.retail.data.services.controller; - -import org.springframework.boot.web.error.ErrorAttributeOptions; -import org.springframework.boot.web.servlet.error.ErrorController; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; - -import java.util.Map; - -@RestController -public class CustomErrorController extends ResponseEntityExceptionHandler implements ErrorController { - - @RequestMapping("/error") - public ResponseEntity> handleError(WebRequest webRequest) { - Map body = getErrorAttributes(webRequest, ErrorAttributeOptions.defaults()); - HttpStatus status = HttpStatus.valueOf((int) body.get("status")); - return new ResponseEntity<>(body, status); - } - - private Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { - // Implement the logic to get error attributes - // This is a placeholder implementation - return Map.of( - "status", 500, - "error", "Internal Server Error", - "message", "An unexpected error occurred", - "error_message", options.toString() - ); - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3a121b3..77fe352 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ rootProject.name = "tech-case-studies" -include("retail-data-services") +include("product-api") include("cart-service") pluginManagement {