InventoryViabilityServiceImpl.java

/*
 * Copyright 2021 Global Crop Diversity Trust
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.gringlobal.service.impl;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Predicate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.api.v1.Pagination;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.Cooperator;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.InventoryAction;
import org.gringlobal.model.InventoryViability;
import org.gringlobal.model.InventoryViabilityAction;
import org.gringlobal.model.InventoryViabilityAttach;
import org.gringlobal.model.InventoryViabilityData;
import org.gringlobal.model.InventoryViabilityRule;
import org.gringlobal.model.OrderRequest;
import org.gringlobal.model.OrderRequestItem;
import org.gringlobal.model.QInventory;
import org.gringlobal.model.QInventoryViability;
import org.gringlobal.model.QInventoryViabilityAction;
import org.gringlobal.model.QInventoryViabilityAttach;
import org.gringlobal.model.QInventoryViabilityData;
import org.gringlobal.model.Site;
import org.gringlobal.model.InventoryViability.ViabilityStatus;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.model.workflow.WorkflowActionStep;
import org.gringlobal.persistence.InventoryRepository;
import org.gringlobal.persistence.InventoryViabilityActionRepository;
import org.gringlobal.persistence.InventoryViabilityDataRepository;
import org.gringlobal.persistence.InventoryViabilityRepository;
import org.gringlobal.persistence.InventoryViabilityRuleRepository;
import org.gringlobal.service.InventoryActionService;
import org.gringlobal.service.InventoryActionService.InventoryActionRequest;
import org.gringlobal.service.InventoryViabilityActionService;
import org.gringlobal.service.InventoryViabilityActionService.*;
import org.gringlobal.service.InventoryViabilityAttachmentService;
import org.gringlobal.service.InventoryViabilityDataService;
import org.gringlobal.service.InventoryViabilityService;
import org.gringlobal.service.MethodService;
import org.gringlobal.service.OrderRequestService;
import org.gringlobal.service.filter.InventoryViabilityActionFilter;
import org.gringlobal.service.filter.InventoryViabilityAttachFilter;
import org.gringlobal.service.filter.InventoryViabilityDataFilter;
import org.gringlobal.service.filter.InventoryViabilityFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
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.google.common.collect.Lists;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.jpa.impl.JPAQuery;
import org.springframework.web.multipart.MultipartFile;

@Service
@Transactional(readOnly = true)
@Slf4j
public class InventoryViabilityServiceImpl extends FilteredCRUDService2Impl<InventoryViability, InventoryViabilityFilter, InventoryViabilityRepository> implements
		InventoryViabilityService {

	@Autowired
	private OrderRequestService orderRequestService;

	@Autowired
	private InventoryActionService inventoryActionService;

	@Autowired
	private InventoryViabilityDataRepository viabilityDataRepository;

	@Autowired
	private InventoryViabilityRuleRepository viabilityRuleRepository;

	@Autowired
	private InventoryRepository inventoryRepository;

	@Component
	protected static class AttachmentSupport extends BaseAttachmentSupport<InventoryViability, InventoryViabilityAttach, InventoryViabilityAttachmentService.InventoryViabilityAttachmentRequest> implements InventoryViabilityAttachmentService {

		public AttachmentSupport() {
			super(QInventoryViabilityAttach.inventoryViabilityAttach.inventoryViability().id, QInventoryViabilityAttach.inventoryViabilityAttach.id);
		}

		@Override
		@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('ViabilityTest', 'WRITE')")
		public InventoryViabilityAttach uploadFile(InventoryViability entity, MultipartFile file, InventoryViabilityAttachmentRequest metadata) throws IOException, InvalidRepositoryPathException, InvalidRepositoryFileDataException {
			return super.uploadFile(entity, file, metadata);
		}

		@Override
		@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('ViabilityTest', 'DELETE')")
		public InventoryViabilityAttach removeFile(InventoryViability entity, Long attachmentId) {
			return super.removeFile(entity, attachmentId);
		}

		@Override
		protected Path createRepositoryPath(InventoryViability inventoryViability) {
			inventoryViability = owningEntityRepository.getReferenceById(inventoryViability.getId());
			return Paths.get("/inventoryViability/" + inventoryViability.getId());
		}

		@Override
		@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('ViabilityTest', 'WRITE')")
		protected InventoryViabilityAttach createAttach(InventoryViability entity, InventoryViabilityAttach source) {
			InventoryViabilityAttach attach = new InventoryViabilityAttach();
			attach.apply(source);
			attach.setVirtualPath(source.getVirtualPath()); // SOAP uses this to create the record
			attach.setInventoryViability(entity);
			return attach;
		}
		
		@Override
		@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('ViabilityTest', 'WRITE')")
		public InventoryViabilityAttach create(InventoryViabilityAttach source) {
			var owningEntity = owningEntityRepository.getReferenceById(source.getInventoryViability().getId());

			var attach = createAttach(owningEntity, source);
			var savedAttach = repository.save(attach);
			return _lazyLoad(savedAttach);
		}

		@Override
		@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('ViabilityTest', 'WRITE')")
		public InventoryViabilityAttach update(InventoryViabilityAttach updated, InventoryViabilityAttach target) {
			target.apply(updated);
			return _lazyLoad(repository.save(target));
		}

		@Override
		@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('ViabilityTest', 'DELETE')")
		public InventoryViabilityAttach remove(InventoryViabilityAttach entity) {
			return super.remove(entity);
		}

		@Override
		@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('ViabilityTest', 'READ')")
		public List<InventoryViabilityAttach> getAllAttachments(InventoryViability inventoryViability) {
			var attachments = Lists.newArrayList(((QuerydslPredicateExecutor<InventoryViabilityAttach>)repository).findAll(QInventoryViability.inventoryViability.id.eq(inventoryViability.getId())));
			attachments.forEach(InventoryViabilityAttach::lazyLoad);
			return attachments;
		}

		@Override
		public Page<InventoryViabilityAttach> list(InventoryViabilityAttachFilter filter, Pageable page) throws SearchException {
			page = Pagination.addSortByParams(page, idSortParams);

			BooleanBuilder predicate = new BooleanBuilder();
			if (filter != null) {
				predicate.and(filter.buildPredicate());
			}

			var entityListQuery = entityListQuery();
			if (entityListQuery != null) {
				JPAQuery<InventoryViabilityAttach> query = entityListQuery.where(predicate);
				return repository.findAll(query, page);
			} else {
				// default implementation without custom loading
				return ((QuerydslPredicateExecutor<InventoryViabilityAttach>)repository).findAll(predicate, page);
			}
		}

		@Override
		protected JPAQuery<InventoryViabilityAttach> entityListQuery() {
			return jpaQueryFactory.selectFrom(QInventoryViabilityAttach.inventoryViabilityAttach)
					// attach cooperator
					.leftJoin(QInventoryViabilityAttach.inventoryViabilityAttach.attachCooperator()).fetchJoin()
					// repository file
					.leftJoin(QInventoryViabilityAttach.inventoryViabilityAttach.repositoryFile()).fetchJoin()
					// inventory viability
					.join(QInventoryViabilityAttach.inventoryViabilityAttach.inventoryViability()).fetchJoin()
				;
		}
	}

	@Override
	public Page<InventoryViability> list(InventoryViabilityFilter filter, Pageable page) throws SearchException {
		return super.list(InventoryViability.class, filter, page);
	}

	@Override
	protected JPAQuery<InventoryViability> entityListQuery() {
		QInventory inv = new QInventory("i");
		return jpaQueryFactory.selectFrom(QInventoryViability.inventoryViability)
				// inventory
				.join(QInventoryViability.inventoryViability.inventory(), inv).fetchJoin()
				// accession
				.join(inv.accession()).fetchJoin()
				// viability rule
				.leftJoin(QInventoryViability.inventoryViability.inventoryViabilityRule()).fetchJoin()
				;
	}

	@Override
	protected NumberPath<Long> entityIdPredicate() {
		return QInventoryViability.inventoryViability.id;
	}

	@Override
//	@PostAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'READ', returnObject.inventoryViability.inventory.site)")
	public InventoryViabilityDetails loadDetails(InventoryViability inventoryViability) {
		var reload = reload(inventoryViability);
		reload.getInventory().lazyLoad();
		if (reload.getInventoryViabilityRule() != null) {
			reload.getInventoryViabilityRule().getId();
		}
		var details = new InventoryViabilityDetails(reload);
		details.datas = reload.getDatas();
		if (details.datas != null) {
			for (var inventoryViabilityData : details.datas) {
				inventoryViabilityData.lazyLoad();
			}
		}
		return details;
	}

	@Override
	public InventoryViabilityDetails calculateResult(InventoryViability iv, Collection<Integer> selectedReplicationNumbers) {
		iv = get(iv);

//		if (iv.getReplicationCount() < 1)
//			throw new InvalidApiUsageException("replicationCount less than 1, nothing to calculate");

		// Get the last observations of each of the replicates (by observation date)
		List<InventoryViabilityData> sumOfObservations = new ArrayList<>();

		var datas = Lists.newArrayList(viabilityDataRepository.findAll(QInventoryViabilityData.inventoryViabilityData.inventoryViability().eq(iv), Sort.by(Sort.Direction.DESC, "countNumber")));
		// distinct replicateCount values
		Collection<Integer> analyzedReplicationNumbers = selectedReplicationNumbers;
		if (CollectionUtils.isEmpty(analyzedReplicationNumbers)) {
			// Find existing replicates
			analyzedReplicationNumbers = datas.stream().map(InventoryViabilityData::getReplicationNumber).distinct().sorted().collect(Collectors.toList());
		}

		// Use selected replicates only
		for (var selectedReplicationNumber : analyzedReplicationNumbers) {
			var sumForReplicate = // filter for replicationNumber
					datas.stream().filter(ivd -> ivd.getReplicationNumber() == selectedReplicationNumber)
					// Make new InventoryViabilityData and summarize the counts across the replicates
					.reduce(new InventoryViabilityData(), InventoryViabilityServiceImpl::summarizeForReplicate);

			sumOfObservations.add(sumForReplicate);

			if (sumForReplicate.sumOfCounts() != sumForReplicate.getReplicationCount().intValue()) {
				throw new InvalidApiUsageException("Sum of counts does not match replicationCount for replication " + selectedReplicationNumber);
			}
		}

		if (sumOfObservations.size() == 0) {
			throw new InvalidApiUsageException("Replication data not available");
		}

		var summarizedIVD = sumOfObservations.stream().reduce(new InventoryViabilityData(), InventoryViabilityServiceImpl::summarize);

		if (summarizedIVD.sumOfCounts() != summarizedIVD.getReplicationCount().intValue()) {
			throw new InvalidApiUsageException("Sum of all counts does not match sum of replicationCounts");
		}

		// After the sums are calculated, a new InventoryViability object is used to calculate values of percent*
		final int totalCount = summarizedIVD.getReplicationCount();

		// Update results of loaded InventoryViability
		InventoryViability result = iv;

		result.setPercentNormal(toPercent(summarizedIVD.getNormalCount(), totalCount));
		result.setPercentHard(toPercent(summarizedIVD.getHardCount(), totalCount));

		result.setPercentUnknown(toPercent(summarizedIVD.getUnknownCount(), totalCount));
		result.setPercentAbnormal(toPercent(summarizedIVD.getAbnormalCount(), totalCount));
		result.setPercentDead(toPercent(summarizedIVD.getDeadCount(), totalCount));
		result.setPercentInfested(toPercent(summarizedIVD.getInfestedCount(), totalCount));
		result.setPercentEmpty(toPercent(summarizedIVD.getEmptyCount(), totalCount));
		// TZ
		result.setPercentTzPositive(toPercent(summarizedIVD.getTzPositiveCount(), totalCount));
		result.setPercentTzNegative(toPercent(summarizedIVD.getTzNegativeCount(), totalCount));

		// Dormant
		// Legacy GG: percent_dormant is calculated from rep_dormant_count + rep_estimated_dormant_count + rep_confirmed_dormant_count
		result.setPercentDormant(toPercent(
			// percent_dormant is calculated from rep_dormant_count + rep_estimated_dormant_count + rep_confirmed_dormant_count
			summarizedIVD.getDormantCount() + summarizedIVD.getEstimatedDormantCount() + summarizedIVD.getConfirmedDormantCount()
			// unlike the original, we include + tz_positive_count
			+ summarizedIVD.getTzPositiveCount()

			// divided by total number of tested seed
			, totalCount));

		// Legacy GG: percent_viable uses rep_normal_count + rep_hard_count + rep_dormant_count + rep_estimated_dormant_count + rep_confirmed_dormant_count
		result.setPercentViable(toPercent(
			// percent_viable uses rep_normal_count + rep_hard_count + rep_dormant_count
			summarizedIVD.getNormalCount() + summarizedIVD.getHardCount()
			// + rep_dormant_count + rep_estimated_dormant_count + rep_confirmed_dormant_count
			+ summarizedIVD.getDormantCount() + summarizedIVD.getEstimatedDormantCount() + summarizedIVD.getConfirmedDormantCount()
			// unlike the original, we include + tz_positive_count
			+ summarizedIVD.getTzPositiveCount()

			// divided by total number of tested seed
			, totalCount));

		var details = new InventoryViabilityDetails(iv);
		details.datas = sumOfObservations;

		return details;
	}

	private double toPercent(int sumColumnCount, int replicationCount) {
		return sumColumnCount * 100. / replicationCount;
	}

	@Override
	@Transactional
	@PostAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'CREATE', returnObject.inventory.site)")
	public InventoryViability createFast(InventoryViability source) {
		assert(source.getId() == null);
		source.setInventory(inventoryRepository.getReferenceById(source.getInventory().getId()));

		var rule = source.getInventoryViabilityRule();
		if (rule != null) {
			var savedRule = viabilityRuleRepository.getReferenceById(source.getInventoryViabilityRule().getId());
			source.setInventoryViabilityRule(savedRule);
			int numberOfReplicates =  Optional.ofNullable(savedRule.getNumberOfReplicates()).orElse(1);
			Integer testQuantity = savedRule.getSeedsPerReplicate() == null ? null : (int) savedRule.getSeedsPerReplicate() * numberOfReplicates;
			source.setReplicationCount(numberOfReplicates);
			source.setTotalTestedCount(testQuantity);
		}

		if (source.getTestedDate() == null) {
			source.setTestedDate(new Date());
		}
		source.setTestedDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);

		var saved = repository.save(source);

		// Start inventory action: Viability test
		inventoryActionService.startAction(InventoryActionRequest.builder()
			.id(Set.of(saved.getInventory().getId()))
			.actionNameCode(CommunityCodeValues.INVENTORY_ACTION_VABILITYTEST.value)
			.build()
		);

		return saved;
	}

	@Override
	@Transactional
	@PostAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'CREATE', returnObject.inventory.site)")
	public InventoryViability create(InventoryViability source) {
		assert(source.getId() == null);
		source.setInventory(inventoryRepository.getReferenceById(source.getInventory().getId()));

		var rule = source.getInventoryViabilityRule();
		if (rule != null) {
			int numberOfReplicates =  Optional.ofNullable(rule.getNumberOfReplicates()).orElse(1);
			Integer testQuantity = rule.getSeedsPerReplicate() == null ? null : (int) rule.getSeedsPerReplicate() * numberOfReplicates;
			source.setReplicationCount(numberOfReplicates);
			source.setTotalTestedCount(testQuantity);
		}

		if (source.getTestedDate() == null) {
			source.setTestedDate(new Date());
		}
		source.setTestedDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);

		var saved = repository.save(source);
		saved.lazyLoad();

		// Start inventory action: Viability test
		inventoryActionService.startAction(InventoryActionRequest.builder()
			.id(Set.of(saved.getInventory().getId()))
			.actionNameCode(CommunityCodeValues.INVENTORY_ACTION_VABILITYTEST.value)
			.build()
		);

		return _lazyLoad(saved);
	}


	@Override
	@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'WRITE', #target.inventory.site)")
	public InventoryViability updateFast(@NotNull @Valid InventoryViability updated, InventoryViability target) {
		
		log.debug("Update InventoryViability. Input data {}", updated);
		target.apply(updated);

		var saved = repository.save(target);
		if (saved.getStatus() == ViabilityStatus.CONCLUSIVE) {
			var request = InventoryActionService.InventoryActionRequest.builder()
				.actionNameCode(CommunityCodeValues.INVENTORY_ACTION_VABILITYTEST.value)
				.id(Set.of(saved.getInventory().getId()))
				.build();

			var inProgress = inventoryActionService.listInProgressActions(request);
			if (inProgress.size() > 0) {
				var completedViabilityAction = inventoryActionService.completeAction(request).get(0);
				log.info("Completed viability action {}", completedViabilityAction.getId());
			}
		}
		return saved;
	}

	@Override
	@PostAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'WRITE', returnObject.inventory.site)")
	public InventoryViability update(InventoryViability updated) {
		return super.update(updated);
	}

	@Override
	@Transactional
	@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'WRITE', #target.inventory.site)")
	public InventoryViability update(InventoryViability input, InventoryViability target) {
		if (input == null) {
			throw new InvalidApiUsageException("Source object must be provided.");
		}

		log.debug("Update InventoryViability. Input data {}", input);
		target.apply(input);

		var saved = repository.save(target);
		if (saved.getStatus() == ViabilityStatus.CONCLUSIVE) {
			var request = InventoryActionService.InventoryActionRequest.builder()
				.actionNameCode(CommunityCodeValues.INVENTORY_ACTION_VABILITYTEST.value)
				.id(Set.of(saved.getInventory().getId()))
				.build();

			var inProgress = inventoryActionService.listInProgressActions(request);
			if (inProgress.size() > 0) {
				var completedViabilityAction = inventoryActionService.completeAction(request).get(0);
				log.info("Completed viability action {}", completedViabilityAction.getId());
			}
		}
		return _lazyLoad(saved);
	}

	@Override
	@Transactional
	@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'DELETE', #entity.inventory.site)")
	public InventoryViability remove(InventoryViability entity) {
		return super.remove(entity);
	}

	@Override
	@Transactional
	@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'ADMINISTRATION', #site)")
	public OrderRequest orderViabilityTest(Site site, Map<Inventory, InventoryViabilityRule> inventoriesAndRules, Cooperator viabilityCooperator) {

//		inventories.forEach((inv) -> {
//			if (! inv.getSite().getId().equals(site.getId())) {
//				throw new InvalidApiUsageException("Inventory does not belong to the site");
//			}
//		});
		final var orderRequestItems = new ArrayList<OrderRequestItem>(inventoriesAndRules.size());

		var inventoryViabilities = inventoriesAndRules.entrySet().stream().map((viabReq) -> {
			var inventory = viabReq.getKey();
			var rule = viabReq.getValue();
			int numberOfReplicates =  Optional.ofNullable(rule.getNumberOfReplicates()).orElse(1);
			Integer testQuantity = rule.getSeedsPerReplicate() == null ? null : (int) rule.getSeedsPerReplicate() * numberOfReplicates;
			
			InventoryViability inventoryViability = new InventoryViability();
			inventoryViability.setInventory(inventory);
			inventoryViability.setInventoryViabilityRule(rule);
			inventoryViability.setTestedDate(new Date());
			inventoryViability.setTestedDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);
			inventoryViability.setReplicationCount(numberOfReplicates);
			inventoryViability.setTotalTestedCount(testQuantity);

			// This will successfully crash without values
			OrderRequestItem ori = new OrderRequestItem();
			ori.setInventory(inventory);
			ori.setQuantityShippedUnitCode(CommunityCodeValues.UNIT_OF_QUANTITY_SEED.value); // Always seed!
			
			if (testQuantity != null) {
				ori.setQuantityShipped(testQuantity.doubleValue());
				ori.setNote(MessageFormat.format("{0,number,integer} replicate(s) of {1,number,integer} seed each = {2,number,integer} seed", numberOfReplicates, rule.getSeedsPerReplicate(), ori.getQuantityShipped()));
			} else {
				ori.setNote("Quantity not specified by Inventory Viability Rule");
			}
			orderRequestItems.add(ori);

			// Start the CommunityCodeValues.INVENTORY_ACTION_VABILITYTEST action by setting the start date
			InventoryActionService.InventoryActionRequest iar = InventoryActionService.InventoryActionRequest.builder()
				.actionNameCode(CommunityCodeValues.INVENTORY_ACTION_VABILITYTEST.value)
				.quantity(testQuantity == null ? null : testQuantity.doubleValue())
				.quantityUnitCode(CommunityCodeValues.UNIT_OF_QUANTITY_SEED.value)
				.formCode(CommunityCodeValues.GERMPLASM_FORM_SEED.value)
				.id(Set.of(inventory.getId()))
				.build();

			var startedActions = inventoryActionService.startAction(iar);
			log.info("Started {} action(s) for {}", startedActions.size(), iar.actionNameCode);

			return inventoryViability;
		}).collect(Collectors.toList());

		// Save
		inventoryViabilities = repository.saveAll(inventoryViabilities);

		var orderRequest = new OrderRequest();
		orderRequest.setOrderTypeCode(CommunityCodeValues.ORDER_REQUEST_TYPE_VIABILITYTEST.value);
		orderRequest.setIntendedUseCode(CommunityCodeValues.ORDER_INTENDED_USE_OTHER.value);

		orderRequest.setRequestorCooperator(viabilityCooperator);
		orderRequest.setShipToCooperator(viabilityCooperator);
		orderRequest.setFinalRecipientCooperator(viabilityCooperator);

		orderRequest = orderRequestService.create(orderRequest, orderRequestItems);

		return orderRequest;
	}

	@Override
	protected void prepareLabelContext(Map<String, Object> context, InventoryViability entity) {
		context.put("inventoryViability", entity);
		context.put("inventory", entity.getInventory());
		context.put("accession", entity.getInventory().getAccession());
		context.put("taxon", entity.getInventory().getAccession().getTaxonomySpecies().getName());
		context.put("inventoryViabilityRule", entity.getInventoryViabilityRule());
	}

	@Service
	@Transactional(readOnly = true)
	protected static class InventoryViabilityDataServiceImpl extends FilteredCRUDService2Impl<InventoryViabilityData, InventoryViabilityDataFilter, InventoryViabilityDataRepository> implements InventoryViabilityDataService {

		@Autowired
		private InventoryViabilityRepository inventoryViabilityRepository;

		@Override
		@Transactional
		@PostAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'WRITE', returnObject.inventoryViability.inventory.site)")
		public InventoryViabilityData createFast(InventoryViabilityData source) {
			assert(source.getId() == null);
			// reload from database: for post-authorize
			source.setInventoryViability(inventoryViabilityRepository.getReferenceById(source.getInventoryViability().getId()));
			var saved = repository.save(source);

			// Validate sum counts
			validateSumOfCounts(saved);
			return saved;
		}

		@Override
		@Transactional
		@PostAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'WRITE', returnObject.inventoryViability.inventory.site)")
		public InventoryViabilityData create(InventoryViabilityData source) {
			assert(source.getId() == null);
			// reload from database: for post-authorize
			source.setInventoryViability(inventoryViabilityRepository.getReferenceById(source.getInventoryViability().getId()));
			var saved = repository.save(source);

			// Validate sum counts
			validateSumOfCounts(saved);

			return _lazyLoad(saved);
		}

		@Override
		@Transactional
		@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'WRITE', #target.inventoryViability.inventory.site)")
		public InventoryViabilityData updateFast(@NotNull @Valid InventoryViabilityData updated, InventoryViabilityData target) {
			target.apply(updated);

			var saved = repository.save(target);

			// Validate sum counts
			validateSumOfCounts(saved);

			return saved;
		}

		@Override
		@Transactional
		@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'WRITE', #target.inventoryViability.inventory.site)")
		public InventoryViabilityData update(InventoryViabilityData input, InventoryViabilityData target) {
			if (input == null) {
				throw new InvalidApiUsageException("Source object must be provided.");
			}

			log.debug("Update InventoryViabilityData. Input data {}", input);
			target.apply(input);

			var saved = repository.save(target);

			// Validate sum counts
			validateSumOfCounts(saved);

			return _lazyLoad(saved);
		}

		@Override
		@Transactional
		@PostAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'DELETE', returnObject.inventoryViability.inventory.site)")
		public InventoryViabilityData remove(InventoryViabilityData entity) {
			return super.remove(entity);
		}

		/**
		 * Load records from the database and check the sums
		 * @param sample InventoryViabilityData to use as sample
		 * @return true of OK
		 * @throws InvalidApiUsageException when the sums do not tally up
		 */
		private void validateSumOfCounts(InventoryViabilityData sample) {
			var qIVD = QInventoryViabilityData.inventoryViabilityData;
			var existingReplicateData = Lists.newArrayList(repository.findAll(
				// match InventoryViablity and replicationNumber
				qIVD.inventoryViability().eq(sample.getInventoryViability()).and(qIVD.replicationNumber.eq(sample.getReplicationNumber())),
				// Sort by countNumber
				qIVD.countNumber.desc()))
				// Summarize (for replicate)
				.stream().reduce(new InventoryViabilityData(), InventoryViabilityServiceImpl::summarizeForReplicate);

			Integer replicationCount = existingReplicateData.getReplicationCount();
			if (replicationCount == null) {
				// nothing to validate
				return;
			}

			if (existingReplicateData.sumOfCounts() > replicationCount.intValue()) {
				throw new InvalidApiUsageException("Sum of counts exceeds replicationCount");
			}
		}
	}

	@Component
	protected static class ActionSupport extends BaseActionSupport<InventoryViability, InventoryViabilityAction, InventoryViabilityActionFilter, InventoryViabilityActionRepository, InventoryViabilityActionRequest, InventoryViabilityActionScheduleFilter>
			implements InventoryViabilityActionService {

		@Autowired
		private InventoryViabilityRepository inventoryViabilityRepository;

		@Autowired
		private MethodService methodService;

		@Override
		protected EntityPath<InventoryViability> getOwningEntityPath() {
			return QInventoryViabilityAction.inventoryViabilityAction.inventoryViability();
		}

		@Override
		protected void applyOwningEntityFilter(InventoryViabilityActionScheduleFilter filter, String owningEntityAlias, List<Predicate> predicates) {
			QInventoryViability qInventoryViability = new QInventoryViability(owningEntityAlias);
			if (predicates != null && filter.inventoryViability != null) {
				predicates.addAll(filter.inventoryViability.collectPredicates(qInventoryViability));
			}
		}

		@Override
		protected InventoryViabilityAction createAction(final InventoryViability owningEntity) {
			var action = new InventoryViabilityAction();
			action.setInventoryViability(owningEntity);
			return action;
		}

		@Override
		protected void updateAction(final InventoryViabilityAction action, final InventoryViabilityActionRequest request) {
			if (request.method != null && !request.method.isNew()) {
				action.setMethod(methodService.get(request.method.getId()));
			}
		}

		@Override
		protected InventoryViabilityAction prepareNextWorkflowStepAction(WorkflowActionStep nextStep, InventoryViabilityAction completedAction) {
			InventoryViabilityAction nextAction = new InventoryViabilityAction();
			nextAction.setInventoryViability(new InventoryViability(completedAction.getInventoryViability().getId()));
			return nextAction;
		}

		@Override
		@Transactional
		@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'ADMINISTRATION')")
		public InventoryViabilityAction create(InventoryViabilityAction source) {
			log.debug("Create InventoryViabilityAction. Input data {}", source);
			InventoryViabilityAction entity = new InventoryViabilityAction();
			entity.apply(source);

			InventoryViabilityAction saved = get(repository.save(entity));
			saved.lazyLoad();

			return saved;
		}

		@Override
		@Transactional
		@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'ADMINISTRATION')")
		public InventoryViabilityAction update(InventoryViabilityAction updated) {
			return super.update(updated);
		}

		@Override
		@Transactional
		@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'ADMINISTRATION')")
		public InventoryViabilityAction remove(InventoryViabilityAction entity) {
			return super.remove(entity);
		}

		@Override
		@Transactional
		@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'ADMINISTRATION')")
		public List<InventoryViabilityAction> reopenAction(InventoryViabilityActionRequest actionData) {
			return super.reopenAction(actionData);
		}

		@Override
		protected void initializeActionDetails(List<InventoryViabilityAction> actions) {
		}

		@Override
		@Transactional
		@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'ADMINISTRATION')")
		public List<InventoryViabilityAction> completeAction(InventoryViabilityActionRequest actionData) {
			return super.completeAction(actionData);
		}

		@Override
		@Transactional
		@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'ADMINISTRATION')")
		public List<InventoryViabilityAction> startAction(InventoryViabilityActionRequest actionData) {
			return super.startAction(actionData);
		}

		@Override
		@Transactional
		@PreAuthorize("@ggceSec.actionAllowed('ViabilityTest', 'ADMINISTRATION')")
		public List<InventoryViabilityAction> scheduleAction(InventoryViabilityActionRequest actionData) {
			return super.scheduleAction(actionData);
		}

		@Override
		protected Iterable<InventoryViability> findOwningEntities(final Set<Long> id) {
			return inventoryViabilityRepository.findAll(QInventoryViability.inventoryViability.id.in(id));
		}
	}

	/**
	 * A customized reducer that will not update the replicationCount if it is already set.
	 */
	private static InventoryViabilityData summarizeForReplicate(InventoryViabilityData summarized, InventoryViabilityData source) {
		var keepReplicationCount = summarized.getReplicationCount();
		var r = summarize(summarized, source);
		if (keepReplicationCount != null) {
			r.setReplicationCount(keepReplicationCount);
		}
		r.setReplicationNumber(source.getReplicationNumber());
		return r;
	}

	/**
	 * Increment summarized *Count fields with values of the extra. The countDate is set to max(countDate)
	 * @param summarized summarized count
	 * @param extra increments
	 * @return updated summarized
	 */
	private static InventoryViabilityData summarize(InventoryViabilityData summarized, InventoryViabilityData extra) {
		if (summarized.getCountDate() == null) {
			summarized.setCountDate(extra.getCountDate());
		} else if (extra.getCountDate() != null && extra.getCountDate().after(summarized.getCountDate())) {
			summarized.setCountDate(extra.getCountDate());
		}
		summarized.setReplicationCount(Optional.ofNullable(summarized.getReplicationCount()).orElse(0) + Optional.ofNullable(extra.getReplicationCount()).orElse(0));

		summarized.setNormalCount(Optional.ofNullable(summarized.getNormalCount()).orElse(0) + Optional.ofNullable(extra.getNormalCount()).orElse(0));
		summarized.setAbnormalCount(Optional.ofNullable(summarized.getAbnormalCount()).orElse(0) + Optional.ofNullable(extra.getAbnormalCount()).orElse(0));
		summarized.setUnknownCount(Optional.ofNullable(summarized.getUnknownCount()).orElse(0) + Optional.ofNullable(extra.getUnknownCount()).orElse(0));

		// Dormant
		summarized.setConfirmedDormantCount(Optional.ofNullable(summarized.getConfirmedDormantCount()).orElse(0) + Optional.ofNullable(extra.getConfirmedDormantCount()).orElse(0));
		summarized.setDormantCount(Optional.ofNullable(summarized.getDormantCount()).orElse(0) + Optional.ofNullable(extra.getDormantCount()).orElse(0));
		summarized.setEstimatedDormantCount(Optional.ofNullable(summarized.getEstimatedDormantCount()).orElse(0) + Optional.ofNullable(extra.getEstimatedDormantCount()).orElse(0));
		summarized.setTreatedDormantCount(Optional.ofNullable(summarized.getTreatedDormantCount()).orElse(0) + Optional.ofNullable(extra.getTreatedDormantCount()).orElse(0));

		// Categories of abnormal
		summarized.setDeadCount(Optional.ofNullable(summarized.getDeadCount()).orElse(0) + Optional.ofNullable(extra.getDeadCount()).orElse(0));
		summarized.setEmptyCount(Optional.ofNullable(summarized.getEmptyCount()).orElse(0) + Optional.ofNullable(extra.getEmptyCount()).orElse(0));
		summarized.setHardCount(Optional.ofNullable(summarized.getHardCount()).orElse(0) + Optional.ofNullable(extra.getHardCount()).orElse(0));
		summarized.setInfestedCount(Optional.ofNullable(summarized.getInfestedCount()).orElse(0) + Optional.ofNullable(extra.getInfestedCount()).orElse(0));

		// Tetrazolium
		summarized.setTzPositiveCount(Optional.ofNullable(summarized.getTzPositiveCount()).orElse(0) + Optional.ofNullable(extra.getTzPositiveCount()).orElse(0));
		summarized.setTzNegativeCount(Optional.ofNullable(summarized.getTzNegativeCount()).orElse(0) + Optional.ofNullable(extra.getTzNegativeCount()).orElse(0));
		return summarized;
	}

	/**
	 * We are generating one label for each replicate in the test.
	 */
	@Override
	protected Collection<String> generateLabelsForEntity(String template, Map<String, Object> params, InventoryViability entity) {
		var ivr = entity.getInventoryViabilityRule();
		Integer replicates = entity.getReplicationCount() == null ? (ivr == null ? null : ivr.getNumberOfReplicates()) : entity.getReplicationCount();
		if (replicates == null) replicates = 1;

		var labelsForReplicates = new ArrayList<String>(replicates);
		for (var i = 0; i < replicates; i++) {
			params.put("replicate", i+1);
			labelsForReplicates.add(templatingService.fillTemplate(template, params));
		}
		return labelsForReplicates;
	}
}