InvitroInventoryApiServiceImpl.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.api.v2.facade.impl;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import javax.validation.Valid;

import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.api.model.InventoryDTO;
import org.gringlobal.api.v2.facade.InvitroInventoryApiService;
import org.gringlobal.api.v2.mapper.MapstructMapper;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.persistence.InventoryMaintenancePolicyRepository;
import org.gringlobal.service.InventoryService;
import org.gringlobal.service.InvitroInventoryService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import org.apache.commons.lang3.StringUtils;

@Service
@Validated
public class InvitroInventoryApiServiceImpl implements InvitroInventoryApiService {

	@Autowired
	private InvitroInventoryService service;

	@Autowired
	private InventoryService inventoryService;

	@Autowired
	private InventoryMaintenancePolicyRepository inventoryMaintPolicyRepo;

	@Autowired
	private MapstructMapper mapper;

	@Override
	@Transactional
	public List<InventoryDTO> invitroStart(IVMultiplicationRequestDTO requestDTO) {
		var request = mapper.map(requestDTO);
		request.sourceInventory = inventoryService.get(request.sourceInventory.getId());
		return mapper.map(service.invitroStart(request), mapper::map);
	}

	@Override
	@Transactional
	public List<InventoryDTO> invitroMultiply(@Valid List<IVMultiplicationRequestDTO> requests) {
		var mappedRequests = requests.stream().map(mapper::map).toList();

		var createdList = new ArrayList<Inventory>();
		for (InvitroInventoryService.IVMultiplicationRequest request : mappedRequests) {
			if (request.multiplicationItems == null || request.multiplicationItems.isEmpty()) {
				throw new InvalidApiUsageException("multiplicationItems must not be null or empty");
			}
			request.sourceInventory = inventoryService.get(request.sourceInventory.getId());

			if (!Objects.equals("Y", request.sourceInventory.getIsAutoDeducted())) {
				// Do not deduct from source (grin-global/grin-global-server#488)
			} else if (request.sourceInventory.getQuantityOnHand() == null || request.sourceInventory.getQuantityOnHand() == 0 || request.sourceInventory.getQuantityOnHand() < request.quantityToDeduct) {
				throw new InvalidApiUsageException("Source inventory does not have sufficient quantity"); // Require quantity
			}

			for (InvitroInventoryService.IVMultiplicationItem item : request.multiplicationItems) {

				item.formTypeCode = item.formTypeCode != null ? item.formTypeCode : request.sourceInventory.getFormTypeCode();
				item.inventoryMaintenancePolicy = item.inventoryMaintenancePolicy != null && item.inventoryMaintenancePolicy.getId() != null ?
					inventoryMaintPolicyRepo.getReferenceById(item.inventoryMaintenancePolicy.getId()) : request.sourceInventory.getInventoryMaintenancePolicy();
				item.inventoryNumberPart3 = item.inventoryNumberPart3 != null ? item.inventoryNumberPart3 : request.sourceInventory.getInventoryNumberPart3();
				item.quantityOnHandUnitCode = item.quantityOnHandUnitCode != null ? item.quantityOnHandUnitCode : request.sourceInventory.getQuantityOnHandUnitCode();
				item.containerTypeCode = item.containerTypeCode != null ? item.containerTypeCode : request.sourceInventory.getContainerTypeCode();

				var inv = new Inventory();
				inv.setParentInventory(request.sourceInventory);
				inv.setAccession(request.sourceInventory.getAccession());
				inv.setFormTypeCode(StringUtils.defaultIfBlank(item.formTypeCode, CommunityCodeValues.GERMPLASM_FORM_INVITRO.value));
				inv.setInventoryMaintenancePolicy(item.inventoryMaintenancePolicy);
				inv.setInventoryNumberPart1(StringUtils.isBlank(item.inventoryNumberPart1) ?
					StringUtils.joinWith(".", request.sourceInventory.getInventoryNumberPart1(), request.sourceInventory.getInventoryNumberPart2())
					: item.inventoryNumberPart1);
				inv.setInventoryNumberPart2(-1L); // Auto-increment
				inv.setInventoryNumberPart3(item.inventoryNumberPart3); // Not sure about this one...
				inv.setQuantityOnHand(item.quantityOnHand);
				inv.setQuantityOnHandUnitCode(item.quantityOnHandUnitCode);
				inv.setIsAutoDeducted(StringUtils.defaultIfBlank(item.isAutoDeducted, request.sourceInventory.getIsAutoDeducted())); // Use parent auto-deducted in multiplication
				inv.setContainerTypeCode(item.containerTypeCode);
				inv.setAvailabilityStatusCode(CommunityCodeValues.INVENTORY_AVAILABILITY_NOTSET.value);
				inv.setSite(request.sourceInventory.getSite());
				inv.setPropagationDate(new Date());
				inv.setPropagationDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);
				inv.setAvailabilityReasonCode(item.availabilityReasonCode);
				inv.setNote(item.note);

				inv.setPathogenStatusCode(item.pathogenStatusCode != null ? item.pathogenStatusCode : request.sourceInventory.getPathogenStatusCode()); // Inherit from the source if not present
				inv.setGeneration(request.sourceInventory.nextGeneration()); // Increment generation

				var created = inventoryService.create(inv);
				service.applyTCExtraFromRequest(created, item, request.sourceInventory);

				createdList.add(created);
			}

			if (Objects.equals("Y", request.sourceInventory.getIsAutoDeducted())) {
				// Consume source inventory
				request.sourceInventory.setQuantityOnHand(request.sourceInventory.getQuantityOnHand() - request.quantityToDeduct);
				request.sourceInventory = inventoryService.update(request.sourceInventory);
			}
		}
		return mapper.map(createdList, mapper::map);
	}
}