InventoryTriggers.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.triggers;
import java.lang.reflect.Field;
import java.time.Duration;
import java.util.Collection;
import java.util.Comparator;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.component.GGCE;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.InventoryMaintenancePolicy;
import org.gringlobal.model.InventoryViability;
import org.gringlobal.model.QInventoryExtra;
import org.gringlobal.model.QInventoryViability;
import org.gringlobal.model.SeedInventoryExtra;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.persistence.InventoryExtraRepository;
import org.gringlobal.persistence.InventoryRepository;
import org.gringlobal.persistence.InventoryViabilityRepository;
import org.gringlobal.service.AccessionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.stereotype.Component;
@Aspect
@Component("inventoryTriggers")
@Slf4j
public class InventoryTriggers {
private final Field FIELD_INVENTORY_NUMBER;
public InventoryTriggers() {
FIELD_INVENTORY_NUMBER = ReflectionUtils.findRequiredField(Inventory.class, "inventoryNumber");
}
@Autowired
private InventoryRepository inventoryRepository;
@Around(value = "(execution(* org.gringlobal.persistence.InventoryRepository.save(..)) || execution(* org.gringlobal.persistence.InventoryRepository.saveAndFlush(..))) && args(inventory)")
public Object aroundInventorySave(final ProceedingJoinPoint joinPoint, final Inventory inventory) throws Throwable {
checkSystemForAccession(inventory);
assignInventoryNumberPart2IfNeeded(inventory);
ReflectionUtils.setField(FIELD_INVENTORY_NUMBER, inventory, GGCE.inventoryNumber(inventory));
if (inventory.isNew()) {
fillMaintenancePolicy(inventory);
}
// update status
updateInventoryStatus(inventory);
return joinPoint.proceed();
}
@Around(value = "(execution(* org.gringlobal.persistence.InventoryRepository.saveAll(Iterable))) && args(inventories)")
public Object aroundInventoriesSave(final ProceedingJoinPoint joinPoint, final Collection<Inventory> inventories) throws Throwable {
log.debug("Many inventories are being saved! {}", inventories);
assignInventoryNumberPart2IfNeeded(inventories);
inventories.forEach(inventory -> ReflectionUtils.setField(FIELD_INVENTORY_NUMBER, inventory, GGCE.inventoryNumber(inventory)));
inventories.stream().filter(Inventory::isNew).forEach(this::fillMaintenancePolicy);
// update status
inventories.forEach(this::updateInventoryStatus);
return joinPoint.proceed();
}
private void checkSystemForAccession(Inventory inventory) {
if (inventory.isSystemInventory()) {
var systemInvForAcce = inventoryRepository.getSystemInventory(inventory.getAccession());
if (systemInvForAcce != null && !Objects.equals(systemInvForAcce.getId(), inventory.getId())) {
throw new InvalidApiUsageException("Accession must have only one system inventory.");
}
}
}
/**
* Assign the next within inventoryNumberPart1
*/
private void assignInventoryNumberPart2IfNeeded(Inventory inventory) {
if (AccessionService.AUTO_GENERATE_VALUE.equals(inventory.getInventoryNumberPart2())) {
long maxPart2 = inventoryRepository.findMaxNumberPart2(inventory.getAccession(), inventory.getInventoryNumberPart1(), inventory.getInventoryNumberPart3(), inventory.getFormTypeCode());
log.debug("Using next numberPart2 {}+1 for inventory {}", maxPart2, inventory.getInventoryNumberPart1());
inventory.setInventoryNumberPart2(1 + maxPart2);
}
}
/**
* Assign the next within inventoryNumberPart1 for collection
*/
private void assignInventoryNumberPart2IfNeeded(Collection<Inventory> inventories) {
inventories.forEach(inventory -> {
if (AccessionService.AUTO_GENERATE_VALUE.equals(inventory.getInventoryNumberPart2())) {
long maxPart2 = inventoryRepository.findMaxNumberPart2(inventory.getAccession(), inventory.getInventoryNumberPart1(), inventory.getInventoryNumberPart3(), inventory.getFormTypeCode());
inventory.setInventoryNumberPart2(1 + maxPart2);
var isNumberTaken = inventories.stream()
.anyMatch(i -> !Objects.equals(inventory, i)
&& Objects.equals(inventory.getInventoryNumberPart1(), i.getInventoryNumberPart1())
&& Objects.equals(inventory.getInventoryNumberPart2(), i.getInventoryNumberPart2())
&& Objects.equals(inventory.getInventoryNumberPart3(), i.getInventoryNumberPart3())
&& Objects.equals(inventory.getFormTypeCode(), i.getFormTypeCode()));
if (isNumberTaken) {
maxPart2 = inventories.stream()
.filter(i -> !Objects.equals(inventory, i)
&& Objects.equals(inventory.getInventoryNumberPart1(), i.getInventoryNumberPart1())
&& Objects.equals(inventory.getInventoryNumberPart3(), i.getInventoryNumberPart3())
&& Objects.equals(inventory.getFormTypeCode(), i.getFormTypeCode()))
.max(Comparator.comparing(Inventory::getInventoryNumberPart2))
.get().getInventoryNumberPart2();
inventory.setInventoryNumberPart2(1 + maxPart2);
}
log.debug("Using next numberPart2 {}+1 for inventory {}", maxPart2, inventory.getInventoryNumberPart1());
}
});
}
private void updateInventoryStatus(final Inventory i) {
if (Inventory.SYSTEM_INVENTORY_FTC.equals(i.getFormTypeCode())) return;
if (i.getIsAutoDeducted().equals("N")) return;
if (i.getQuantityOnHand() == null) {
i.setIsAvailable("N");
// i.setAvailabilityStatusCode(CommunityCodeValues.INVENTORY_AVAILABILITY_LOWINVENTORY.value); // Maybe?
return;
}
if (i.getDistributionCriticalQuantity() == null) {
if (i.getQuantityOnHand().doubleValue() > 0) {
i.setIsAvailable("Y"); // Has quantity but no limits
} else {
i.setIsAvailable("N"); // Has 0 quantity
}
return;
}
double quantityOnHand = i.getQuantityOnHand().doubleValue();
double distributionCriticalQuantity = i.getDistributionCriticalQuantity().doubleValue();
if (quantityOnHand < distributionCriticalQuantity) {
i.setIsAvailable("N");
if (Set.of(CommunityCodeValues.INVENTORY_AVAILABILITY_AVAILABLE.value, CommunityCodeValues.INVENTORY_AVAILABILITY_NOTSET.value).contains(i.getAvailabilityStatusCode().toUpperCase())) {
i.setAvailabilityStatusCode(CommunityCodeValues.INVENTORY_AVAILABILITY_LOWINVENTORY.value);
}
} else {
i.setIsAvailable("Y");
if (Set.of(CommunityCodeValues.INVENTORY_AVAILABILITY_LOWINVENTORY.value, CommunityCodeValues.INVENTORY_AVAILABILITY_NOTSET.value).contains(i.getAvailabilityStatusCode().toUpperCase())) {
i.setAvailabilityStatusCode(CommunityCodeValues.INVENTORY_AVAILABILITY_AVAILABLE.value);
}
}
}
/**
* Fill maintenance policy values
*
* @param inventory The inventory to insert
*/
public void fillMaintenancePolicy(final Inventory inventory) {
final InventoryMaintenancePolicy maintenancePolicy = inventory.getInventoryMaintenancePolicy();
if (StringUtils.isBlank(inventory.getFormTypeCode())) {
inventory.setFormTypeCode(maintenancePolicy.getFormTypeCode());
}
if (StringUtils.isBlank(inventory.getQuantityOnHandUnitCode())) {
inventory.setQuantityOnHandUnitCode(maintenancePolicy.getQuantityOnHandUnitCode());
}
if (StringUtils.isBlank(inventory.getWebAvailabilityNote())) {
inventory.setWebAvailabilityNote(maintenancePolicy.getWebAvailabilityNote());
}
if (StringUtils.isBlank(inventory.getIsAutoDeducted())) {
inventory.setIsAutoDeducted(maintenancePolicy.getIsAutoDeducted());
}
if (StringUtils.isBlank(inventory.getDistributionDefaultFormCode())) {
inventory.setDistributionDefaultFormCode(maintenancePolicy.getDistributionDefaultFormCode());
}
if (inventory.getDistributionDefaultQuantity() == null) {
inventory.setDistributionDefaultQuantity(maintenancePolicy.getDistributionDefaultQuantity());
}
if (StringUtils.isBlank(inventory.getDistributionUnitCode())) {
inventory.setDistributionUnitCode(maintenancePolicy.getDistributionUnitCode());
}
if (inventory.getDistributionCriticalQuantity() == null) {
inventory.setDistributionCriticalQuantity(maintenancePolicy.getDistributionCriticalQuantity());
}
if (inventory.getRegenerationCriticalQuantity() == null) {
inventory.setRegenerationCriticalQuantity(maintenancePolicy.getRegenerationCriticalQuantity());
}
}
@Autowired
private InventoryViabilityRepository inventoryViabilityRepository;
@Autowired
private InventoryExtraRepository inventoryExtraRepository;
/**
* Max number of days between propagation and viability test to consider it as "initial seed viability"
*/
@Value("${initial.viability.max.days:365}")
private long maxDaysForInitialViability;
@After(value = "(execution(* org.gringlobal.persistence.InventoryViabilityRepository.save(..)) || execution(* org.gringlobal.persistence.InventoryViabilityRepository.saveAndFlush(..))) && args(inventoryViability)")
public void afterInventoryViabilitySave(final InventoryViability inventoryViability) {
if (inventoryViability.getId() == null) return; // Avoid Hibernate AssertionFailure "null id in org.gringlobal.model.InventoryViability"
upsertSeedInventoryExtraWithLatestViability(inventoryViability.getInventory(), null);
}
@After(value = "(execution(* org.gringlobal.persistence.InventoryViabilityRepository.saveAll(Iterable))) && args(viabilityCollection)")
public void afterInventoryViabilityCollectionSave(final Collection<InventoryViability> viabilityCollection) {
var viabilityInventories = viabilityCollection.stream().map(InventoryViability::getInventory).collect(Collectors.toSet());
viabilityInventories.forEach(i -> {
if (i.getId() == null) return; // Avoid Hibernate AssertionFailure "null id in org.gringlobal.model.InventoryViability"
upsertSeedInventoryExtraWithLatestViability(i, null);
});
}
/**
* Find the latest (by {@code testedDate}) completed (with {@code percentViable}) viability result and
* apply it to {@code inventory.extra.lastViability}.
*
* @param inventory The inventory to update
*/
private void upsertSeedInventoryExtraWithLatestViability(Inventory inventory, Set<Long> excludeInventoryViabilityIds) {
QInventoryViability qViability = QInventoryViability.inventoryViability;
var q = qViability.inventory().eq(inventory)
.and(qViability.percentViable.isNotNull())
.and(qViability.testedDate.isNotNull())
.and(qViability.status.eq(InventoryViability.ViabilityStatus.CONCLUSIVE));
if (CollectionUtils.isNotEmpty(excludeInventoryViabilityIds)) {
q = q.and(qViability.id.notIn(excludeInventoryViabilityIds));
}
var viabilityAfterSaved = inventoryViabilityRepository.findAll(
q,
PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "testedDate")) // get latest completed test
);
var viabilityForExtra = viabilityAfterSaved.get().findFirst().orElse(null);
var existingExtra = inventoryExtraRepository.findOne(QInventoryExtra.inventoryExtra.inventory().eq(inventory));
if (existingExtra.isPresent()) {
var extra = (SeedInventoryExtra) existingExtra.get();
extra.setLastViability(viabilityForExtra);
if (viabilityForExtra != null) { // It's possible that viabilityForExtra is null
extra.setLastViabilityResult(viabilityForExtra.getPercentViable());
extra.setLastViabilityDate(viabilityForExtra.getTestedDate());
extra.setLastViabilityDateCode(viabilityForExtra.getTestedDateCode());
updateInitialViability(extra, viabilityForExtra);
}
inventoryExtraRepository.save(extra);
} else if (viabilityForExtra != null) {
SeedInventoryExtra extra = new SeedInventoryExtra();
extra.setInventory(inventory);
extra.setLastViability(viabilityForExtra);
extra.setLastViabilityResult(viabilityForExtra.getPercentViable());
extra.setLastViabilityDate(viabilityForExtra.getTestedDate());
extra.setLastViabilityDateCode(viabilityForExtra.getTestedDateCode());
updateInitialViability(extra, viabilityForExtra);
inventoryExtraRepository.save(extra);
}
}
/**
* Update {@code initialViability} if not yet set.
* @param extra the extra
* @param inventoryViability the latest viability test, can be null in which case no update happens
*/
private void updateInitialViability(SeedInventoryExtra extra, InventoryViability inventoryViability) {
if (inventoryViability == null) {
return; // Nothing to do here
}
if (extra.getInitialViability() != null) {
return; // Do not change anything
}
if (inventoryViability.getTestedDate() == null) {
return; // We don't have the date when the test was completed
}
if (extra.getInventory().getPropagationDate() == null) {
return; // We don't have the date when this seed inventory came to life
}
var timeDiff = Duration.between(extra.getInventory().getPropagationDate().toInstant(), inventoryViability.getTestedDate().toInstant());
log.debug("Difference between viability test date and propagation date is: {}", timeDiff);
if (timeDiff.compareTo(Duration.ofDays(maxDaysForInitialViability)) <= 0) {
log.info("{} days passed between propagation and viability test, this is initial seed viability.", timeDiff.toDays());
extra.setInitialViability(inventoryViability.getPercentViable());
extra.setInitialViabilityDate(inventoryViability.getTestedDate());
extra.setInitialViabilityDateCode(inventoryViability.getTestedDateCode());
} else {
log.debug("{} days passed between propagation and viability test, this is NOT initial seed viability.", timeDiff.toDays());
}
}
@Around(value = "(execution(* org.gringlobal.persistence.InventoryViabilityRepository.delete(..)) || execution(* org.gringlobal.persistence.InventoryViabilityRepository.deleteInBatch(..))) && args(inventoryViability)")
public Object aroundInventoryViabilityDelete(final ProceedingJoinPoint joinPoint, final InventoryViability inventoryViability) throws Throwable {
upsertSeedInventoryExtraWithLatestViability(inventoryViability.getInventory(), Set.of(inventoryViability.getId()));
return joinPoint.proceed();
}
@Around(value = "(execution(* org.gringlobal.persistence.InventoryViabilityRepository.deleteAll(..)) || execution(* org.gringlobal.persistence.InventoryViabilityRepository.deleteAllInBatch(..))) && args(viabilityCollection)")
public Object aroundInventoryViabilityDeleteAll(final ProceedingJoinPoint joinPoint, final Collection<InventoryViability> viabilityCollection) throws Throwable {
var deletedIds = viabilityCollection.stream().map(InventoryViability::getId).collect(Collectors.toSet());
viabilityCollection.forEach(iv -> upsertSeedInventoryExtraWithLatestViability(iv.getInventory(), deletedIds));
return joinPoint.proceed();
}
}