ExternalRequestApiServiceImpl.java

/*
 * Copyright 2025 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.genesys.server.api.v2.facade.impl;

import lombok.extern.slf4j.Slf4j;

import org.genesys.blocks.oauth.persistence.OAuthClientRepository;
import org.genesys.server.api.v2.facade.ExternalRequestApiService;
import org.genesys.server.api.v2.mapper.MapstructMapper;
import org.genesys.server.api.v2.model.impl.ExternalRequestDTO;
import org.genesys.server.api.v2.model.impl.ExternalRequestGenesysDTO;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.model.genesys.AccessionRef;
import org.genesys.server.model.impl.ExternalRequest;
import org.genesys.server.model.impl.ExternalRequestAccessionRef;
import org.genesys.server.model.impl.QExternalRequest;
import org.genesys.server.persistence.ExternalRequestAccessionRefRepository;
import org.genesys.server.persistence.ExternalRequestRepository;
import org.genesys.server.service.ArticleService;
import org.genesys.server.service.ArticleTranslationService;
import org.genesys.server.service.ContentService;
import org.genesys.server.service.EMailService;
import org.genesys.server.service.worker.AccessionRefMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.validation.Valid;

@Service
@Slf4j
@Validated
public class ExternalRequestApiServiceImpl implements ExternalRequestApiService {

	private static final long DAYS_BEFORE_EXPIRATION = 7;

	@Value("${frontend.url}")
	private String frontendUrl;

	@Autowired
	private ExternalRequestRepository externalRequestRepository;

	@Autowired
	private ExternalRequestAccessionRefRepository requestAccessionRefRepository;

	@Autowired
	private MapstructMapper mapstructMapper;

	@Autowired
	private AccessionRefMatcher accessionRefMatcher;

	@Autowired
	private ArticleService articleService;

	@Autowired
	private ContentService contentService;

	@Autowired
	private EMailService emailService;

	@Autowired
	private OAuthClientRepository oAuthClientRepository;

	@Override
	@Transactional(rollbackFor = { ExternalRequestAccessionsException.class })
	public UUID registerExternalRequest(@Valid ExternalRequestDTO request, String remoteAddress) throws ExternalRequestAccessionsException {

		List<ExternalRequestAccessionRef> requestAcceRefs = mapstructMapper.map(request.getItems(), mapstructMapper::map);

		try {
			log.info("Rematching {} dataset refs", requestAcceRefs.size());
			accessionRefMatcher.rematchAccessionRefs(requestAcceRefs, requestAccessionRefRepository);
		} catch (Throwable e) {
			log.info("Rematch failed with {}", e.getMessage(), e);
		}

		var cannotBeRequested = requestAcceRefs.stream().filter(ref -> {
			var accession = ref.getAccession();
			if (accession == null) return true; // No such accession in Genesys
			if (Objects.equals(true, accession.isHistoric())) return true; // Accession is historical
			if (Objects.equals(false, accession.getAvailable())) return true; // Accession is not available for distribution
			if (Objects.equals(false, accession.getInstitute().isAllowMaterialRequests())) return true; // Institute does not enable requests via Genesys
			return false;
		}).map(mapstructMapper::map).collect(Collectors.toList());
		if (cannotBeRequested.size() > 0) {
			// Some of the material cannot be requested through Genesys
			throw new ExternalRequestAccessionsException(cannotBeRequested);
		}

		// Register the request
		var externalRequest = new ExternalRequest();
		externalRequest.setRemoteAddress(remoteAddress);
		externalRequest.setUuid(UUID.randomUUID());
		var pii = request.getPii();
		externalRequest.setEmail(pii.getEmail());
		externalRequest.setPid(pii.getPid());
		externalRequest.setName(pii.getName());

		var savedRequest = externalRequestRepository.save(externalRequest);

		requestAcceRefs.forEach(requestAcceRef -> requestAcceRef.setRequest(savedRequest));

		var savedAcceRefs = requestAccessionRefRepository.saveAll(requestAcceRefs);

		var client = oAuthClientRepository.getReferenceById(savedRequest.getCreatedBy());

		final ArticleTranslationService.TranslatedArticle article = articleService.getGlobalArticle(ContentService.SMTP_FROM_EXTERNAL_MATERIAL_REQUEST, LocaleContextHolder.getLocale());

		if (article != null) {
			final Map<String, Object> root = new HashMap<>();
			root.put("origin", client.getTitle());
			root.put("frontendUrl", frontendUrl);
			root.put("accessions", savedAcceRefs.stream().map(AccessionRef::getAccession).collect(Collectors.toList()));
			root.put("requestKey", savedRequest.getUuid());

			var body = article.getTranslation() != null ? article.getTranslation().getBody() : article.getEntity().getBody();
			var title = article.getTranslation() != null ? article.getTranslation().getTitle() : article.getEntity().getTitle();

			final String mailBody = contentService.processTemplate(body, root);
			final String mailSubject = title + " [" + savedRequest.getUuid() + "]";
			log.debug(">>>{}", mailBody);

			emailService.sendMail(mailSubject, mailBody, savedRequest.getEmail());
		}

		return savedRequest.getUuid();
	}

	@Override
	@Transactional(readOnly = true)
	public boolean externalRequestIsAvailable(UUID key) throws NotFoundElement {
		if (!externalRequestRepository.exists(
			QExternalRequest.externalRequest.uuid.eq(key)
				.and(QExternalRequest.externalRequest.createdDate.after(Instant.now().minus(DAYS_BEFORE_EXPIRATION, ChronoUnit.DAYS)))
				.and(QExternalRequest.externalRequest.materialRequest().isNull()))) {
			throw new NotFoundElement();
		}
		return true;
	}

	@Override
	@Transactional(readOnly = true)
	public ExternalRequestGenesysDTO getRequest(UUID key) throws NotFoundElement {
		var request = externalRequestRepository.findByUuid(key);
		if (request == null || request.getMaterialRequest() != null || request.getCreatedDate().isBefore(Instant.now().minus(DAYS_BEFORE_EXPIRATION, ChronoUnit.DAYS))) {
			throw new NotFoundElement();
		}
		return mapstructMapper.mapGenesys(request);
	}
}