AssignFulfillmentUseCase.java

package com.fulfilment.application.monolith.fulfillment.domain.usecases;

import com.fulfilment.application.monolith.fulfillment.domain.models.FulfillmentAssignment;
import com.fulfilment.application.monolith.fulfillment.domain.ports.AssignFulfillmentOperation;
import com.fulfilment.application.monolith.fulfillment.domain.ports.FulfillmentAssignmentStore;
import jakarta.enterprise.context.ApplicationScoped;
import com.fulfilment.application.monolith.exception.HttpStatus;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
import java.util.Set;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;

/**
 * Validates business constraints and persists a new fulfillment assignment.
 *
 * <p>Three constraints are enforced (in order):
 * <ol>
 *   <li><b>Max 2 warehouses per product per store</b>: a product may be fulfilled by at most
 *       2 distinct warehouses within the same store.</li>
 *   <li><b>Max 3 warehouses per store</b>: across all products, a store may be served by at most
 *       3 distinct warehouses.</li>
 *   <li><b>Max 5 product types per warehouse</b>: a warehouse may fulfill at most 5 distinct
 *       product types (regardless of which stores they serve).</li>
 * </ol>
 *
 * <p>A duplicate assignment (same store + product + warehouse triple) is rejected with 409
 * before constraint checks run.
 */
@ApplicationScoped
public class AssignFulfillmentUseCase implements AssignFulfillmentOperation {

  private static final Logger LOGGER = Logger.getLogger(AssignFulfillmentUseCase.class.getName());

  static final int MAX_WAREHOUSES_PER_PRODUCT_PER_STORE = 2;
  static final int MAX_WAREHOUSES_PER_STORE = 3;
  static final int MAX_PRODUCT_TYPES_PER_WAREHOUSE = 5;

  private final FulfillmentAssignmentStore assignmentStore;

  public AssignFulfillmentUseCase(FulfillmentAssignmentStore assignmentStore) {
    this.assignmentStore = assignmentStore;
  }

  @Override
  public FulfillmentAssignment assign(FulfillmentAssignment assignment) {
    LOGGER.debugf(
        "Attempting to assign warehouse '%s' to fulfill product %d for store %d",
        assignment.warehouseCode, assignment.productId, assignment.storeId);

    // 1. Duplicate guard
    if (assignmentStore.exists(
        assignment.storeId, assignment.productId, assignment.warehouseCode)) {
      LOGGER.warnf(
          "Duplicate assignment: warehouse '%s', product %d, store %d already exists",
          assignment.warehouseCode, assignment.productId, assignment.storeId);
      throw new WebApplicationException(
          "Fulfillment assignment already exists for store "
              + assignment.storeId
              + ", product "
              + assignment.productId
              + ", warehouse '"
              + assignment.warehouseCode
              + "'.",
          Response.Status.CONFLICT);
    }

    // Pre-fetch the data needed for all three constraint checks
    // (findByStore is called once and reused for constraints 1 and 2)
    java.util.List<FulfillmentAssignment> storeAssignments =
        assignmentStore.findByStore(assignment.storeId);
    java.util.List<FulfillmentAssignment> warehouseAssignments =
        assignmentStore.findByWarehouse(assignment.warehouseCode);

    // 2. Constraint: each Product can be fulfilled by max 2 Warehouses per Store
    Set<String> warehousesForProductInStore =
        storeAssignments.stream()
            .filter(a -> a.productId.equals(assignment.productId))
            .map(a -> a.warehouseCode)
            .collect(Collectors.toSet());

    // After the duplicate check we know this warehouseCode is not yet in the set,
    // so the set size equals the current count of distinct fulfilling warehouses.
    if (warehousesForProductInStore.size() >= MAX_WAREHOUSES_PER_PRODUCT_PER_STORE) {
      LOGGER.warnf(
          "Store %d already has %d warehouses fulfilling product %d (max %d)",
          assignment.storeId,
          warehousesForProductInStore.size(),
          assignment.productId,
          MAX_WAREHOUSES_PER_PRODUCT_PER_STORE);
      throw new WebApplicationException(
          "Product "
              + assignment.productId
              + " already has the maximum number of fulfilling warehouses ("
              + MAX_WAREHOUSES_PER_PRODUCT_PER_STORE
              + ") for store "
              + assignment.storeId
              + ".",
          HttpStatus.UNPROCESSABLE_ENTITY);
    }

    // 3. Constraint: each Store can be fulfilled by max 3 Warehouses
    Set<String> warehousesForStore =
        storeAssignments.stream()
            .map(a -> a.warehouseCode)
            .collect(Collectors.toSet());

    // Only a violation if the warehouse we're adding is brand-new to this store.
    // If it already serves the store for a different product, the distinct count doesn't increase.
    if (!warehousesForStore.contains(assignment.warehouseCode)
        && warehousesForStore.size() >= MAX_WAREHOUSES_PER_STORE) {
      LOGGER.warnf(
          "Store %d already has %d distinct warehouses (max %d)",
          assignment.storeId, warehousesForStore.size(), MAX_WAREHOUSES_PER_STORE);
      throw new WebApplicationException(
          "Store "
              + assignment.storeId
              + " already has the maximum number of fulfilling warehouses ("
              + MAX_WAREHOUSES_PER_STORE
              + ").",
          HttpStatus.UNPROCESSABLE_ENTITY);
    }

    // 4. Constraint: each Warehouse can store max 5 product types
    Set<Long> productsForWarehouse =
        warehouseAssignments.stream()
            .map(a -> a.productId)
            .collect(Collectors.toSet());

    // Only a violation if the product we're adding is brand-new to this warehouse.
    if (!productsForWarehouse.contains(assignment.productId)
        && productsForWarehouse.size() >= MAX_PRODUCT_TYPES_PER_WAREHOUSE) {
      LOGGER.warnf(
          "Warehouse '%s' already stores %d distinct product types (max %d)",
          assignment.warehouseCode,
          productsForWarehouse.size(),
          MAX_PRODUCT_TYPES_PER_WAREHOUSE);
      throw new WebApplicationException(
          "Warehouse '"
              + assignment.warehouseCode
              + "' already stores the maximum number of product types ("
              + MAX_PRODUCT_TYPES_PER_WAREHOUSE
              + ").",
          HttpStatus.UNPROCESSABLE_ENTITY);
    }

    assignmentStore.create(assignment);
    LOGGER.infof(
        "Fulfillment assignment created: warehouse '%s' fulfills product %d for store %d (id=%d)",
        assignment.warehouseCode, assignment.productId, assignment.storeId, assignment.id);

    return assignment;
  }
}