Organization Hooks & Stock Aggregation: Implementation Guide

Alex Johnson
-
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 Organization is saved, the following steps are executed:
    1. A new Shelf entity is created. This entity represents the default shelf.
    2. The name of the shelf is set to "General". This provides a clear and recognizable name for the default storage location.
    3. The description is set to "Default shelf for all products". This description clarifies the purpose of the shelf for users.
    4. The deletable property is set to false. This ensures that the default shelf cannot be accidentally deleted, maintaining the integrity of the storage system.
    5. The shelf is saved to the database. This persists the new shelf entity, making it available for use.
    6. The creation event is logged in ActivityLogger. This provides an audit trail of shelf creation, aiding in tracking and debugging.
// 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 the shelfId (UUID) and shelfName (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 specific shelfId. It represents the quantity of a product on a specific shelf.
  • ConsolidatedProductResponse.java: This DTO consolidates product information across all shelves. It includes the productGroupId (UUID), productName (String), totalQuantity (Integer), a list of shelfInstances (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 the shelfId (UUID) and shelfName (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. The shelfId serves as a unique identifier, while the shelfName provides 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 the shelfId to 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 the productGroupId (a UUID that groups similar products), the productName (a user-friendly name for the product), the totalQuantity (the sum of the product across all shelves), a list of shelfInstances (detailed information about the product on each shelf, using ProductShelfInstanceResponse), and a list of shelvesWithProduct (a quick lookup list of shelves where the product is located, using ShelfSummaryResponse). 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 accept shelfId and categoryId filters.

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:

  1. 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.
  2. Check for productGroupId: The method then checks if a productGroupId is provided. The productGroupId is used to group similar products together. If it’s null, it means that the request is for a specific product instance, and the method proceeds accordingly.
  3. Handle Single Instance (If productGroupId is Null): If the productGroupId is null, the method returns just the information for the target product. This is straightforward: the product’s details are encapsulated in a ProductShelfInstanceResponse, and a list containing only this instance is returned.
  4. Handle Multiple Instances (If productGroupId Exists): If a productGroupId is 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 same productGroupId as 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 of shelfInstances. This involves creating a ProductShelfInstanceResponse for each instance of the product on each shelf. Each ProductShelfInstanceResponse includes the quantity of the product on that shelf, the shelf ID, and any other relevant details.
  5. Return the Aggregated Data: Finally, the method returns a list of ProductShelfInstanceResponse objects, 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.

  1. 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.
  2. Return Distinct ShelfSummaryResponse Objects: 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 of ShelfSummaryResponse objects. 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.

  1. Update Method Signature: The first step is to update the method signature to accept shelfId and categoryId as nullable parameters. This means that these parameters can be either provided by the user or left null if no filtering is desired.
  2. Utilize findAllByOrganizationIdWithFilters: The method then uses the findAllByOrganizationIdWithFilters repository method, which was implemented in a previous issue. This method is designed to handle filtering based on various criteria, including shelfId and categoryId. 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 /products endpoint needs to be updated to accept @RequestParam for shelfId and categoryId. 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 for shelfId and categoryId. The @RequestParam annotation 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-instances returns the correct totalQuantity sum across multiple shelves. This validates the aggregated view implementation.
  • [ ] GET /products allows filtering by a specific Shelf ID. This confirms the filtering capability by shelf.
  • [ ] GET /products allows filtering by a specific Category 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.

You may also like