KPINotifications.java

/*
 * Copyright 2024 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.notification;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import org.gringlobal.model.Cooperator;
import org.gringlobal.model.SysUser;
import org.gringlobal.model.kpi.DimensionKey;
import org.gringlobal.model.kpi.ExecutionRun;
import org.gringlobal.model.kpi.QExecution;
import org.gringlobal.model.notification.NotificationSchedule;
import org.gringlobal.notification.schedule.ScheduledNotification;
import org.gringlobal.persistence.kpi.ExecutionRepository;
import org.gringlobal.persistence.kpi.ExecutionRunRepository;
import org.gringlobal.service.AppResourceService;
import org.gringlobal.service.EMailService;
import org.gringlobal.service.TemplatingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Send the latest KPI execution results by email.
 * 
 * The list of recipients is compiled from SIDs that are mentioned in the permissions list, permissions are not checked.
 */
@Component
@Slf4j
public class KPINotifications {

	@Autowired
	private ExecutionRepository executionRepository;

	@Autowired
	private ExecutionRunRepository executionRunRepository;
	
	@Autowired
	private EMailService emailService;

	@Autowired
	private TemplatingService templatingService;

	@Autowired
	private AppResourceService appResourceService;

	@Value("${frontend.url}")
	private String frontendUrl;
	
	private static final String KPI_EXECUTION_URL = "/admin/kpi/";

	@ScheduledNotification
//	@SchedulerLock(name = "org.gringlobal.notification.KPINotifications")
//	@Scheduled(cron = "0 0 12 ? * MON-FRI", zone = "UTC") // Execute every MON-FRI at 12:00 UTC
	// @Scheduled(fixedDelay = 10000) // For testing
	@Transactional(readOnly = true)
	public void sendAllKPIs(NotificationSchedule schedule, List<SysUser> recipients) throws Exception {
		log.info("Generating scheduled notifications for KPI runs");
		var activeExecutions = executionRepository.findAll(QExecution.execution.isActive.eq("Y"));
		for (var execution : activeExecutions) {
			notifyLastKPIExecution(execution.getName(), recipients);
		}
	}

	@Transactional(readOnly = true)
	public void notifyLastKPIExecution(String executionName, List<SysUser> recipients) throws Exception {
		var execution = executionRepository.findByName(executionName);
		log.info("Generating last KPI run notification for: {} {}", execution.getName(), execution.getTitle());
		
		var sysUserRecipients = recipients.stream()
			.map(SysUser::getCooperator) // To cooperator
			.filter(Objects::nonNull)
			.peek(cooperator -> log.debug("Notifiying {}", cooperator))
			.map(Cooperator::getEmail) // To email
			.filter(StringUtils::isNotBlank)
			.collect(Collectors.toSet());
		
		if (sysUserRecipients.isEmpty()) return;
		log.info("Sending last KPI run notification to: {}", sysUserRecipients);
		
		var lastRuns = executionRunRepository.findLast(execution, Pageable.ofSize(1)).getContent();
		if (lastRuns.isEmpty()) return;
		log.info("Sending last KPI run of {} to {} emails", execution.getName(), sysUserRecipients.size());

		ExecutionRun lastRun = lastRuns.get(0);

		List<String> dimensionNames = new ArrayList<>();
		var observations = lastRun.getObservations();
		if (observations != null && !observations.isEmpty()) {
			var dimensions = observations.get(0).getDimensions();
			if (dimensions != null && !dimensions.isEmpty()) {
				dimensionNames = dimensions.stream().map(DimensionKey::getName).collect(Collectors.toList());
			}
		}

		Map<String, Object> templateParams = new HashMap<>();
		templateParams.put("execution", execution);
		templateParams.put("executionRun", lastRun);
		templateParams.put("dimensionNames", dimensionNames);
		templateParams.put("kpiExecutionURL", frontendUrl + KPI_EXECUTION_URL + executionName);
		var resource = appResourceService.getResource(AppResourceService.APP_NAME_GGCE, "notification/kpi-execution-template.mustache", Locale.getDefault());
		String template;
		if (resource != null) {
			template = resource.getDisplayMember();
		} else {
			log.info("AppResource 'notification/kpi-execution-template.mustache' not found, using bundled template");
			template = Files.readString(Path.of(getClass().getResource("/notification/kpi-execution-template.mustache").getPath()));
		}
		
		var messageBody = templatingService.fillTemplate(template, templateParams);

		for (var emailTo : sysUserRecipients) {
			emailService.sendMail(execution.getTitle(), messageBody, emailTo);
		}
	}

}