Organization Hooks & Stock Aggregation: Implementation Guide
This article details the implementation of organization hooks and aggregated views for stock levels within the Stock Keeping App. This feature ensures that every new organization automatically has a default storage location and provides a unified view of product stock levels across all shelves. Users can see the total quantity of a product and drill down into the specific locations where those items are stored. Let's dive into the technical requirements and steps involved.
Branch Strategy
Before we start, let's clarify the branching strategy:
- Base Branch:
feat/shelves(already exists) - Working Branch: Create your branch from
feat/shelves(e.g.,feat/shelf-aggregated-views) - Merge Target:
feat/shelves
Make sure you're working on the correct branch to avoid conflicts and ensure a smooth integration process.
Technical Requirements
This implementation involves several key components, including the organization service hook, DTO layer, service layer read operations, and controller layer updates. We'll break down each requirement in detail.
1. Organization Service Hook: Automatically Creating Default Shelves
In this section, we'll focus on modifying the createOrganization method in src/main/java/team/soma/stockkeeper/service/organization/OrganizationServiceImpl.java. The goal is to automatically create a default "General" shelf whenever a new organization is created.
When a new organization is created in the Stock Keeping App, it’s essential to have a default storage location ready. This ensures that products can be immediately assigned to a shelf without requiring manual setup. The Organization Service Hook handles this by automatically creating a "General" shelf.
- Logic Implementation: Immediately after a new
Organizationis saved, the following steps are executed:- A new
Shelfentity is created. This entity represents the default shelf. - The
nameof the shelf is set to "General". This provides a clear and recognizable name for the default storage location. - The
descriptionis set to "Default shelf for all products". This description clarifies the purpose of the shelf for users. - The
deletableproperty is set tofalse. This ensures that the default shelf cannot be accidentally deleted, maintaining the integrity of the storage system. - The shelf is saved to the database. This persists the new shelf entity, making it available for use.
- The creation event is logged in
ActivityLogger. This provides an audit trail of shelf creation, aiding in tracking and debugging.
- A new
// Example code snippet (Illustrative)
@Override
public Organization createOrganization(Organization organization) {
Organization savedOrganization = organizationRepository.save(organization);
Shelf generalShelf = new Shelf();
generalShelf.setOrganization(savedOrganization);
generalShelf.setName("General");
generalShelf.setDescription("Default shelf for all products");
generalShelf.setDeletable(false);
shelfRepository.save(generalShelf);
activityLogger.log("Created General shelf for organization: " + savedOrganization.getName());
return savedOrganization;
}
This ensures that every organization starts with a basic storage setup, streamlining the onboarding process and reducing manual configuration.
2. DTO Layer: Structuring Data for Aggregated Views
The DTO (Data Transfer Object) layer is crucial for structuring the data that will be presented in the aggregated views. This layer defines the format of the data exchanged between the service and controller layers. We need to create three new DTOs:
ShelfSummaryResponse.java: This DTO provides a summary of a shelf, including theshelfId(UUID) andshelfName(String). It's used to quickly identify shelves in the UI.ProductShelfInstanceResponse.java: This DTO contains details for a single shelf row, such as the quantity, threshold, and the specificshelfId. It represents the quantity of a product on a specific shelf.ConsolidatedProductResponse.java: This DTO consolidates product information across all shelves. It includes theproductGroupId(UUID),productName(String),totalQuantity(Integer), a list ofshelfInstances(List), and a quick lookup list of shelvesWithProduct(List).
The DTO layer is integral in ensuring that data is structured efficiently for both internal processing and external presentation. Here's a breakdown of the newly created DTOs:
-
ShelfSummaryResponse.java: This DTO acts as a concise representation of a shelf. By including only theshelfId(UUID) andshelfName(String), it minimizes the amount of data transferred when a full shelf object isn’t necessary. This is particularly useful in scenarios where a list of shelves needs to be displayed without needing all the details of each shelf. TheshelfIdserves as a unique identifier, while theshelfNameprovides a human-readable label.// Example code snippet public class ShelfSummaryResponse { private UUID shelfId; private String shelfName; // Constructors, getters, and setters } -
ProductShelfInstanceResponse.java: This DTO is designed to capture specific details about a product's presence on a single shelf. It includes the quantity of the product on the shelf, the threshold level for that product on the shelf, and theshelfIdto identify the specific shelf. This DTO is crucial for understanding the distribution of a product across different shelves and for managing stock levels and reordering points.// Example code snippet public class ProductShelfInstanceResponse { private Integer quantity; private Integer threshold; private UUID shelfId; // Constructors, getters, and setters } -
ConsolidatedProductResponse.java: This DTO aggregates all relevant information about a product across all shelves. It includes theproductGroupId(a UUID that groups similar products), theproductName(a user-friendly name for the product), thetotalQuantity(the sum of the product across all shelves), a list ofshelfInstances(detailed information about the product on each shelf, usingProductShelfInstanceResponse), and a list ofshelvesWithProduct(a quick lookup list of shelves where the product is located, usingShelfSummaryResponse). This DTO provides a comprehensive view of a product’s stock levels and distribution, making it invaluable for inventory management and reporting.// Example code snippet public class ConsolidatedProductResponse { private UUID productGroupId; private String productName; private Integer totalQuantity; private List<ProductShelfInstanceResponse> shelfInstances; private List<ShelfSummaryResponse> shelvesWithProduct; // Constructors, getters, and setters }
By using these DTOs, the application can efficiently transfer and process data, ensuring that the information is well-structured and easy to work with. This is crucial for creating scalable and maintainable software.
3. Service Layer (Read Operations): Implementing Business Logic
The service layer is where the core business logic resides. Here, we implement the read operations required to fetch and aggregate product data. This includes implementing the following methods:
getProductShelfInstances: Retrieves product instances across shelves.getProductShelvesByName: Retrieves shelves where a product name exists.- Update
getAllProducts: Modifies the method signature to acceptshelfIdandcategoryIdfilters.
getProductShelfInstances
This method is responsible for retrieving the distribution of a product across different shelves. The logic involves fetching the target product and then, depending on whether a productGroupId is provided, either returning just that product's instance or aggregating quantities for all products sharing the same group ID. Here’s a detailed breakdown:
- Retrieve the Target Product: The method starts by fetching the specific product for which the shelf instances are being requested. This is typically done using a repository query that retrieves the product by its unique identifier.
- Check for
productGroupId: The method then checks if aproductGroupIdis provided. TheproductGroupIdis used to group similar products together. If it’snull, it means that the request is for a specific product instance, and the method proceeds accordingly. - Handle Single Instance (If
productGroupIdis Null): If theproductGroupIdisnull, the method returns just the information for the target product. This is straightforward: the product’s details are encapsulated in aProductShelfInstanceResponse, and a list containing only this instance is returned. - Handle Multiple Instances (If
productGroupIdExists): If aproductGroupIdis provided, the method needs to aggregate the quantities of all products sharing that ID across all shelves. This involves the following steps:- Find All Products with the Same
productGroupId: A repository query is used to fetch all products that have the sameproductGroupIdas the target product. - Sum Quantities: The method iterates through the list of products and sums their quantities. This gives the total quantity of the product group across all locations.
- Map to
shelfInstances: The list of products is then mapped to a list ofshelfInstances. This involves creating aProductShelfInstanceResponsefor each instance of the product on each shelf. EachProductShelfInstanceResponseincludes the quantity of the product on that shelf, the shelf ID, and any other relevant details.
- Find All Products with the Same
- Return the Aggregated Data: Finally, the method returns a list of
ProductShelfInstanceResponseobjects, providing a detailed view of how the product (or product group) is distributed across shelves.
getProductShelvesByName
This method focuses on finding all shelves that contain products with a specific name within an organization. This is particularly useful for inventory management and reporting, allowing users to quickly identify where products of a certain type are stored.
- Find Products by Name: The method starts by querying the database to find all products within the organization that have the specified name. This is typically done using a repository query that filters products by their name and organization ID.
- Return Distinct
ShelfSummaryResponseObjects: Once the products are retrieved, the method processes the results to extract the shelves on which these products are located. Instead of returning full shelf objects, it returns a list ofShelfSummaryResponseobjects. This DTO includes only the shelf ID and name, minimizing the amount of data transferred. The list is also made distinct to avoid duplicates, ensuring that each shelf is listed only once.
Update getAllProducts
Updating the getAllProducts method involves enhancing its filtering capabilities by allowing users to filter products based on shelfId and categoryId. This provides more granular control over the product listing, making it easier to find specific items.
- Update Method Signature: The first step is to update the method signature to accept
shelfIdandcategoryIdas nullable parameters. This means that these parameters can be either provided by the user or leftnullif no filtering is desired. - Utilize
findAllByOrganizationIdWithFilters: The method then uses thefindAllByOrganizationIdWithFiltersrepository method, which was implemented in a previous issue. This method is designed to handle filtering based on various criteria, includingshelfIdandcategoryId. By delegating the filtering logic to this repository method, the service layer method remains clean and focused on orchestrating the request.
4. Controller Layer: Exposing API Endpoints
The controller layer acts as the entry point for external requests. Here, we update the ProductManagementController in src/main/java/team/soma/stockkeeper/controller/ to expose new API endpoints and update existing ones.
The controller layer serves as the interface between the application's business logic and the outside world. By mapping HTTP requests to specific service methods, the controller ensures that user interactions are correctly processed and that appropriate responses are returned. In this section, we focus on updating the ProductManagementController to expose new API endpoints and enhance existing ones, thereby providing the necessary functionality for managing product stock levels and locations.
-
New Endpoints: We need to add two new GET endpoints:
GET /products/{productId}/shelf-instances: This endpoint retrieves the shelf instances for a specific product, providing a detailed view of how the product is distributed across different shelves. It is essential for understanding the stock levels of a product in various locations.GET /products/by-name/{productName}/shelves: This endpoint retrieves a list of shelves where a product with the given name exists. It is useful for quickly identifying the locations where a particular product is stored, aiding in inventory management and stocktaking.
-
Updated Endpoint: The existing
GET /productsendpoint needs to be updated to accept@RequestParamforshelfIdandcategoryId. This enhancement allows users to filter the list of products based on the shelf and category, providing more granular control over product listings.GET /products: This endpoint, which retrieves a list of products, is updated to accept optional query parameters forshelfIdandcategoryId. The@RequestParamannotation is used to map these query parameters to method parameters, allowing the controller to filter the product list based on these criteria. This enhancement makes it easier for users to find products within specific shelves or categories.
// Example code snippet (Illustrative)
@RestController
@RequestMapping("/products")
public class ProductManagementController {
@GetMapping("/{productId}/shelf-instances")
public ResponseEntity<ConsolidatedProductResponse> getProductShelfInstances(@PathVariable UUID productId) {
// Implementation to call service and return response
}
@GetMapping("/by-name/{productName}/shelves")
public ResponseEntity<List<ShelfSummaryResponse>> getProductShelvesByName(@PathVariable String productName) {
// Implementation to call service and return response
}
@GetMapping
public ResponseEntity<List<Product>> getAllProducts(
@RequestParam(required = false) UUID shelfId,
@RequestParam(required = false) UUID categoryId) {
// Implementation to call service and return response
}
}
By implementing these new and updated endpoints, the controller layer ensures that the application provides a comprehensive and flexible API for managing product stock levels and locations.
Acceptance Criteria
To ensure the implementation meets the requirements, the following acceptance criteria must be met:
- [ ] Creating a new Organization automatically generates a "General" shelf. This verifies the organization service hook functionality.
- [ ] The "General" shelf cannot be deleted via the delete endpoint (tested in Issue 1, verified here). This ensures the integrity of the default shelf.
- [ ]
GET /products/{id}/shelf-instancesreturns the correcttotalQuantitysum across multiple shelves. This validates the aggregated view implementation. - [ ]
GET /productsallows filtering by a specificShelf ID. This confirms the filtering capability by shelf. - [ ]
GET /productsallows filtering by a specificCategory ID. This confirms the filtering capability by category. - [ ]
GET /products/by-name/...returns a list of shelves where that product name exists. This validates the product name-based shelf retrieval.
Conclusion
Implementing organization hooks and aggregated views for stock levels is a significant enhancement to the Stock Keeping App. By automatically creating a default "General" shelf and providing a consolidated view of product stock levels, we streamline inventory management and improve the user experience. This guide has walked through the technical requirements, from the organization service hook to the controller layer, and outlined the acceptance criteria to ensure successful implementation.
For more information on REST API design and best practices, check out this resource.