OrderRequestItemServiceImpl.java
/*
* Copyright 2026 Global Crop Diversity Trust
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root folder or http://www.apache.org/licenses/LICENSE-2.0
*/
package org.gringlobal.service.impl;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.OrderRequest;
import org.gringlobal.model.OrderRequestItem;
import org.gringlobal.model.OrderRequestItemAction;
import org.gringlobal.model.QInventory;
import org.gringlobal.model.QOrderRequestItem;
import org.gringlobal.model.QOrderRequestItemAction;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.model.community.SecurityAction;
import org.gringlobal.model.workflow.WorkflowActionStep;
import org.gringlobal.persistence.OrderRequestItemActionRepository;
import org.gringlobal.persistence.OrderRequestItemRepository;
import org.gringlobal.persistence.OrderRequestRepository;
import org.gringlobal.service.InventoryService;
import org.gringlobal.service.OrderRequestItemActionService;
import org.gringlobal.service.OrderRequestItemActionService.OrderRequestItemActionRequest;
import org.gringlobal.service.OrderRequestItemActionService.OrderRequestItemActionScheduleFilter;
import org.gringlobal.service.OrderRequestItemService;
import org.gringlobal.service.filter.OrderRequestFilter;
import org.gringlobal.service.filter.OrderRequestItemActionFilter;
import org.gringlobal.service.filter.OrderRequestItemFilter;
import org.gringlobal.service.filter.SiteFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.jpa.impl.JPAQuery;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.Hibernate;
/**
* The OrderRequestItemServiceImpl.
*/
@Transactional(readOnly = true)
@Service
@Slf4j
public class OrderRequestItemServiceImpl extends FilteredCRUDService2Impl<OrderRequestItem, OrderRequestItemFilter, OrderRequestItemRepository> implements OrderRequestItemService {
@Autowired
private InventoryService inventoryService;
@Autowired
private OrderRequestRepository orderRequestRepository;
@Component
protected static class ActionSupport extends BaseActionSupport<OrderRequestItem, OrderRequestItemAction, OrderRequestItemActionFilter, OrderRequestItemActionRepository, OrderRequestItemActionRequest, OrderRequestItemActionScheduleFilter>
implements OrderRequestItemActionService {
@Autowired
private OrderRequestRepository orderRequestRepository;
@Autowired
private OrderRequestItemRepository orderRequestItemRepository;
@Override
protected EntityPath<OrderRequestItem> getOwningEntityPath() {
return QOrderRequestItemAction.orderRequestItemAction.orderRequestItem();
}
@Override
protected OrderRequestItemActionFilter adjustFilter(OrderRequestItemActionFilter filter) {
var siteIds = ggceSec.getSiteIds(SecurityAction.Request.name(), "READ");
if (siteIds != null) {
log.debug("Forcing RequestItem Site IDs: {}", siteIds);
var siteFilter = new OrderRequestItemActionFilter()
.orderRequestItem(new OrderRequestItemFilter()
.orderRequest(new OrderRequestFilter()
.site(new SiteFilter()
.id(siteIds))
)
)
.AND(filter);
return siteFilter;
}
return filter;
}
@Override
protected void initializeActionDetails(List<OrderRequestItemAction> actions) {
actions.forEach(action -> {
Hibernate.initialize(action.getOrderRequestItem().getInventory());
Hibernate.initialize(action.getOrderRequestItem().getInventory().getAccession());
});
}
@Override
protected void applyOwningEntityFilter(OrderRequestItemActionScheduleFilter filter, String owningEntityAlias, List<Predicate> predicates) {
QOrderRequestItem qOrderRequestItem = new QOrderRequestItem(owningEntityAlias);
if (predicates != null && filter.orderRequestItem != null) {
predicates.addAll(filter.orderRequestItem.collectPredicates(qOrderRequestItem));
}
}
@Override
protected OrderRequestItemAction createAction(OrderRequestItem owningEntity) {
OrderRequestItemAction action = new OrderRequestItemAction();
action.setOrderRequestItem(owningEntity);
return action;
}
@Override
protected void updateAction(OrderRequestItemAction action, OrderRequestItemActionRequest request) {
action.setActionCost(request.actionCost);
action.setActionInformation(request.actionInformation);
}
@Override
protected OrderRequestItemAction prepareNextWorkflowStepAction(WorkflowActionStep nextStep, OrderRequestItemAction completedAction) {
OrderRequestItemAction nextAction = new OrderRequestItemAction();
nextAction.setOrderRequestItem(new OrderRequestItem(completedAction.getOrderRequestItem().getId()));
return createFast(nextAction);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.orderRequestItem.orderRequest.site)")
public OrderRequestItemAction create(OrderRequestItemAction source) {
return _lazyLoad(createFast(source));
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.orderRequestItem.orderRequest.site)")
public OrderRequestItemAction createFast(OrderRequestItemAction source) {
log.debug("Create OrderRequestItemAction. Input data {}", source);
OrderRequestItemAction entity = new OrderRequestItemAction();
entity.apply(source);
entity.setOrderRequestItem(orderRequestItemRepository.getReferenceById(source.getOrderRequestItem().getId()));
return repository.save(entity);
}
@Override
protected Iterable<OrderRequestItem> findOwningEntities(Set<Long> id) {
return orderRequestItemRepository.findAll(QOrderRequestItem.orderRequestItem.id.in(id));
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.![orderRequestItem.orderRequest.site])")
public List<OrderRequestItemAction> scheduleAction(OrderRequestItemActionRequest actionData) {
return super.scheduleAction(actionData);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.![orderRequestItem.orderRequest.site])")
public List<OrderRequestItemAction> startAction(OrderRequestItemActionRequest actionData) {
return super.startAction(actionData);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.![orderRequestItem.orderRequest.site])")
public List<OrderRequestItemAction> completeAction(OrderRequestItemActionRequest actionData) {
return super.completeAction(actionData);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.![orderRequestItem.orderRequest.site])")
public List<OrderRequestItemAction> cancelAction(OrderRequestItemActionRequest actionData) {
return super.cancelAction(actionData);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.![orderRequestItem.orderRequest.site])")
public List<OrderRequestItemAction> reopenAction(OrderRequestItemActionRequest actionData) {
return super.reopenAction(actionData);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.orderRequestItem.orderRequest.site)")
public OrderRequestItemAction update(OrderRequestItemAction updated, OrderRequestItemAction target) {
return super.update(updated, target);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.orderRequestItem.orderRequest.site)")
public OrderRequestItemAction updateFast(OrderRequestItemAction updated, OrderRequestItemAction target) {
return super.updateFast(updated, target);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.orderRequestItem.orderRequest.site)")
public OrderRequestItemAction remove(OrderRequestItemAction target) {
return super.remove(target);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.![orderRequestItem.orderRequest.site])")
public List<OrderRequestItemAction> assignActions(Map<Long, String> actionAssignee) {
return super.assignActions(actionAssignee);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', returnObject.![orderRequestItem.orderRequest.site])")
public List<OrderRequestItemAction> startWorkflow(long workflowId, Set<Long> owningEntities) {
return super.startWorkflow(workflowId, owningEntities);
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', #orderRequest.site)")
public List<OrderRequestItemAction> startActions(OrderRequest orderRequest, String actionCode) {
OrderRequestItemActionRequest request = new OrderRequestItemActionRequest();
var disallowedStates = Set.of(CommunityCodeValues.ORDER_REQUEST_ITEM_STATUS_CANCEL.value, CommunityCodeValues.ORDER_REQUEST_ITEM_STATUS_SPLIT.value);
orderRequest = orderRequestRepository.getReferenceById(orderRequest.getId());
request.id = orderRequest.getOrderRequestItems().stream()
// filter for allowed codes = exclude CANCEL and SPLIT items
.filter((ori) -> ! disallowedStates.contains(ori.getStatusCode()))
// to ID
.map(OrderRequestItem::getId).collect(Collectors.toSet());
request.actionNameCode = actionCode;
if (CollectionUtils.isEmpty(request.id)) {
return List.of();
}
return super.startAction(request);
}
@Override
@PreAuthorize("@ggceSec.actionAllowed('RequestItem', 'ADMINISTRATION', #orderRequest.site)")
public List<OrderRequestItemAction> scheduleActions(OrderRequest orderRequest, OrderRequestItemActionRequest actionData, OrderRequestItemFilter itemFilter) {
var disallowedStates = Set.of(CommunityCodeValues.ORDER_REQUEST_ITEM_STATUS_CANCEL.value, CommunityCodeValues.ORDER_REQUEST_ITEM_STATUS_SPLIT.value);
var filter = new OrderRequestItemFilter();
filter.AND = itemFilter;
filter.orderRequest(new OrderRequestFilter());
filter.orderRequest.id(Set.of(orderRequest.getId()));
filter.NOT(new OrderRequestItemFilter());
filter.NOT().statusCode(disallowedStates);
var qOri = QOrderRequestItem.orderRequestItem;
actionData.id = new HashSet<>(jpaQueryFactory.select(qOri.id).from(qOri).where(filter.nestedPredicate(qOri)).fetch());
log.info("Scheduling filtered item actions for {} ORI: {}", actionData.id.size(), actionData);
if (CollectionUtils.isEmpty(actionData.id)) {
return List.of();
}
return super.scheduleAction(actionData);
}
@Override
public Page<OrderRequestItemAction> listByOrderRequest(OrderRequest orderRequest, Pageable page) {
assert(orderRequest != null);
assert(orderRequest.isNew() == false);
final var orderRequestItemAction = new QOrderRequestItemAction("oria");
final var orderRequestItem = new QOrderRequestItem("ori");
var query = jpaQueryFactory.selectFrom(orderRequestItemAction)
.join(orderRequestItemAction.orderRequestItem(), orderRequestItem).fetchJoin()
.join(orderRequestItem.inventory()).fetchJoin()
.leftJoin(orderRequestItemAction.cooperator()).fetchJoin()
.where(orderRequestItem.orderRequest().eq(orderRequest));
var totalElements = query.fetchCount();
// apply sorting
for (Sort.Order o : page.getSort()) {
query.orderBy(new OrderSpecifier<>(o.isAscending() ? Order.ASC : Order.DESC, ExpressionUtils.path(String.class, orderRequestItemAction, o.getProperty())));
}
// apply pagination
if (page.isPaged()) {
query.offset(page.getOffset()).limit(page.getPageSize());
}
return new PageImpl<>(query.fetch(), page, totalElements);
}
}
@Override
protected NumberPath<Long> entityIdPredicate() {
return QOrderRequestItem.orderRequestItem.id;
}
@Override
public Page<OrderRequestItem> list(OrderRequestItemFilter filter, Pageable page) throws SearchException {
return super.list(OrderRequestItem.class, filter, page);
}
@Override
protected JPAQuery<OrderRequestItem> entityListQuery() {
QInventory inventory = QOrderRequestItem.orderRequestItem.inventory();
QInventory withdrawnInventory = QOrderRequestItem.orderRequestItem.withdrawnInventory();
return jpaQueryFactory.selectFrom(QOrderRequestItem.orderRequestItem)
.leftJoin(inventory).fetchJoin()
.leftJoin(withdrawnInventory).fetchJoin()
// .leftJoin(QOrderRequestItem.orderRequestItem.sourceCooperator()).fetchJoin()
;
}
@Override
protected OrderRequestItemFilter adjustFilter(OrderRequestItemFilter filter) {
var siteIds = ggceSec.getSiteIds(SecurityAction.Request.name(), "READ");
if (siteIds != null) {
log.debug("Forcing RequestItem Site IDs: {}", siteIds);
var siteFilter = new OrderRequestItemFilter()
.orderRequest(new OrderRequestFilter()
.site(new SiteFilter()
.id(siteIds)))
.AND(filter);
return siteFilter;
}
return filter;
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'CREATE', returnObject.orderRequest.site)")
public OrderRequestItem createFast(OrderRequestItem source) {
log.debug("Create order request item. Input data {}", source);
var orderRequestItem = new OrderRequestItem();
orderRequestItem.apply(source);
assert (orderRequestItem.getInventory() != null && orderRequestItem.getInventory().getId() != null);
orderRequestItem.setInventory(inventoryService.get(orderRequestItem.getInventory().getId()));
validateInventoryMatchesRequestSite(orderRequestItem);
return repository.save(orderRequestItem);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'CREATE', returnObject.orderRequest.site)")
public OrderRequestItem create(OrderRequestItem source) {
return _lazyLoad(createFast(source));
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('RequestItem', 'WRITE', #target.orderRequest.site)")
@PostAuthorize("@ggceSec.actionAllowed('RequestItem', 'WRITE', returnObject.orderRequest.site)")
public OrderRequestItem updateFast(@NotNull @Valid OrderRequestItem updated, OrderRequestItem target) {
log.debug("Update OrderRequestItem. Input data {}", updated);
if (Objects.isNull(updated.getInventory()) || Objects.isNull(updated.getInventory().getId())) {
throw new InvalidApiUsageException("OrderRequestItem does not specify inventory");
}
updated.setInventory(inventoryService.get(updated.getInventory().getId()));
// save order request
updated.setOrderRequest(target.getOrderRequest());
validateInventoryMatchesRequestSite(updated);
if (updated.getWithdrawnInventory() != null && updated.getWithdrawnInventory().getId() != null) {
updated.setWithdrawnInventory(inventoryService.get(updated.getWithdrawnInventory().getId()));
}
target.apply(updated);
assert(target.getInventory().equals(updated.getInventory()));
if (StringUtils.isBlank(target.getExternalTaxonomy())) {
target.setExternalTaxonomy(target.getInventory().getAccession().getTaxonomySpecies().getName());
}
return repository.save(target);
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('RequestItem', 'WRITE', #target.orderRequest.site)")
public OrderRequestItem update(OrderRequestItem updated, OrderRequestItem target) {
return _lazyLoad(updateFast(updated, target));
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR')")
public List<OrderRequestItem> removeWithdrawnInventories(Set<Long> itemIds) {
var items = repository.findAllById(itemIds);
items.stream().map(OrderRequestItem::getWithdrawnInventory).forEach(inventoryService::remove);
return repository.findAllById(itemIds);
}
@Override
protected void prepareLabelContext(Map<String, Object> context, OrderRequestItem entity) {
context.put("orderRequestItem", entity);
context.put("orderRequest", entity.getOrderRequest());
context.put("inventory", entity.getInventory());
context.put("withdrawnInventory", entity.getWithdrawnInventory());
}
private void validateInventoryMatchesRequestSite(OrderRequestItem item) {
if (item.getOrderRequest() == null || item.getOrderRequest().getId() == null) {
return;
}
var inventory = item.getInventory();
if (inventory == null) {
throw new InvalidApiUsageException("OrderRequestItem does not specify inventory");
}
if (Objects.equals(Inventory.SYSTEM_INVENTORY_FTC, inventory.getFormTypeCode())) {
// Accessions can be at any site
return;
}
var orderRequest = orderRequestRepository.findById(item.getOrderRequest().getId()).orElse(null);
if (orderRequest == null || orderRequest.getSite() == null) {
return;
}
if (inventory.getSite() == null || !Objects.equals(inventory.getSite().getId(), orderRequest.getSite().getId())) {
throw new InvalidApiUsageException("Inventory does not belong to the order request site. " +
"Split the order request by site before assigning inventory from another site.");
}
}
}