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;
}
}