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.");
		}
	}
}