# SvaSaleForecastTask.java

    package cloud.wconnect.workperformance.businesstask.sva.saleforecast;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.NotifyDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.scheduler.JobParameterDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.NotifyInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JobParameterUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.workperformance.entity.sva.SvaScheduler;
    import cloud.wconnect.workperformance.entity.sva.saleforecast.SaleForecast;
    import cloud.wconnect.workperformance.entity.sva.setting.FirstDay;
    import cloud.wconnect.workperformance.pojo.vo.sva.salesforcast.SaleForecastEmailVO;
    import cloud.wconnect.workperformance.service.sva.saleforecast.ISaleForecastService;
    import cloud.wconnect.workperformance.service.sva.scheduleactual.ISvaSendNotifyService;
    import cloud.wconnect.workperformance.service.sva.setting.IFirstDayService;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Resource;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Component;

    import java.time.ZonedDateTime;
    import java.time.format.TextStyle;
    import java.util.Comparator;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.function.Function;

    import static java.util.stream.Collectors.toMap;

    /**
     * 定时任务：SVA - Sale Forecast
     *
     * @author zc
     * @date 2023/03/28
     */
    @Slf4j
    @Component
    public class SvaSaleForecastTask {
        @Resource
        private ISaleForecastService saleForecastService;
        @Resource
        private ISvaSendNotifyService sendNotifyService;
        @Resource
        private IFirstDayService firstDayService;
        @Resource
        private NotifyInternalService notifyInternalService;
        @Resource
        private PropertyI18nRedisService propertyI18nRedisService;
        @Resource
        private ObjectMapper objectMapper;

        @Value("${default.first-day.sva-setting}")
        private Integer startOfWeek;

        @Value("${here.link.sva.sale-forecast}")
        private String link;

        /**
         * 定时任务：检查每个 property 的 Sale Forecast 数据是否按时填写
         *
         * <p>
         * 每天的 17:00:00 检查数据是否按时填写，
         * 若未按时填写，则发送邮件通知、记录异常日志
         * </p>
         *
         * @param schedulerJob 调度数据
         * @author zc
         * @date 2023/3/28
         */
        @Async
        public void dataMissingCheck(SvaScheduler schedulerJob) {
            log.info("SvaSaleForecastTask.dataMissingCheck start...");

            int propertyId = schedulerJob.getPropertyId();
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            if (!propertyDTO.getStatus()) {
                log.warn("SvaSaleForecastTask.dataMissingCheck end... the property is closed, no processing required");
                return;
            }

            JobParameterDTO jobParameterDTO = JsonUtils.convertStringToObject(objectMapper,
                    schedulerJob.getParameters(), JobParameterDTO.class);
            // 获取周起始日数据
            FirstDay firstDay = firstDayService.get(propertyId);
            // 获取报告日期范围
            Map<String, Long> dateRange = JobParameterUtils.parseDateRange(jobParameterDTO.getDateRange(),
                    jobParameterDTO.getDateRangeUnit(), propertyDTO.getTimezone(),
                    Objects.nonNull(firstDay) ? firstDay.getFirstDay() : startOfWeek);

            // 生成未创建的 Sale Forecast 信息集合
            List<SaleForecast> newSaleForecasts = generateNewSaleForecasts(propertyDTO, dateRange);
            if (CollectionUtils.isEmpty(newSaleForecasts)) {
                log.info("SvaSaleForecastTask.dataMissingCheck end... newSaleForecasts is empty");
                return;
            }

            // 保存未创建的 Sale Forecast 信息
            saleForecastService.saveBatch(newSaleForecasts);

            // 获取通知配置
            String missDate = getMissDate(propertyDTO, newSaleForecasts);
            String hereLink = link + "?tenant=" + propertyDTO.getTenantSymbol() + "&property=" + propertyDTO.getAbbr();
            List<SaleForecastEmailVO> saleForecastEmailVOList = convertToVoWithSort(propertyDTO, newSaleForecasts);
            NotifyDTO notify = sendNotifyService.getNotifyDTOForSales(propertyDTO, missDate, hereLink, saleForecastEmailVOList);
            if (Objects.isNull(notify)) {
                log.info("SvaSaleForecastTask.dataMissingCheck end... notifyDTO is null");
                return;
            }

            // 发送通知
            notifyInternalService.sendNotify(notify);

            log.info("SvaSaleForecastTask.dataMissingCheck end...");
        }

        /**
         * 将 Sale Forecast 转换为邮件通知 VO 实例
         *
         * @param propertyDTO   资产
         * @param saleForecasts 数据
         * @return 邮件通知VO实例集合，按照日期升序排序
         * @author zc
         * @date 2024/10/29
         */
        private List<SaleForecastEmailVO> convertToVoWithSort(PropertyDTO propertyDTO, List<SaleForecast> saleForecasts) {
            List<SaleForecastEmailVO> voList = Lists.newArrayListWithExpectedSize(saleForecasts.size());

            String language = propertyDTO.getLanguage();
            String longDate = propertyDTO.getInternationalization().getDateTime().getLongDate();
            saleForecasts.stream().sorted(Comparator.comparing(SaleForecast::getTheDate)).forEach(saleForecast -> {
                ZonedDateTime theDateZoned = DateTimeUtils.toZonedDateTime(saleForecast.getTheDate(), propertyDTO.getTimezone());
                SaleForecastEmailVO saleForecastEmailVO = new SaleForecastEmailVO();
                saleForecastEmailVO.setTheDate(InternationalizationUtils.formatDateTime(saleForecast.getTheDate(), longDate,
                        StringUtils.EMPTY, propertyDTO.getTimezone(), language));
                saleForecastEmailVO.setDayOfWeek(theDateZoned.getDayOfWeek().getDisplayName(TextStyle.FULL,
                        InternationalizationUtils.getLocale(language)));

                voList.add(saleForecastEmailVO);
            });
            return voList;
        }

        /**
         * 获取缺失日期
         *
         * @param propertyDTO      资产
         * @param saleForecastList 数据
         * @return java.lang.String
         * @author zc
         * @date 2024/10/29
         */
        private String getMissDate(PropertyDTO propertyDTO, List<SaleForecast> saleForecastList) {
            String language = propertyDTO.getLanguage();
            String longDate = propertyDTO.getInternationalization().getDateTime().getLongDate();
            Long startDate = saleForecastList.stream().min(Comparator.comparing(SaleForecast::getTheDate))
                    .map(SaleForecast::getTheDate).orElse(null);
            Long endDate = saleForecastList.stream().max(Comparator.comparing(SaleForecast::getTheDate))
                    .map(SaleForecast::getTheDate).orElse(null);
            if (Objects.isNull(startDate) || Objects.isNull(endDate)) {
                return StringUtils.EMPTY;
            }
            return InternationalizationUtils.formatDateTime(startDate, longDate, StringUtils.EMPTY, propertyDTO.getTimezone(), language) +
                    " - " + InternationalizationUtils.formatDateTime(endDate, longDate, StringUtils.EMPTY, propertyDTO.getTimezone(), language);
        }

        /**
         * 生成未创建的 Sale Forecast 信息集合
         *
         * @param property  property
         * @param dateRange 报告数据日期范围
         * @return Sale Forecast 信息集合
         * @author zc
         * @date 2024/3/16
         */
        private List<SaleForecast> generateNewSaleForecasts(PropertyDTO property, Map<String, Long> dateRange) {
            long startDateTimestamp = dateRange.get(JobParameterDTO.START_DATE);
            long endDateTimestamp = dateRange.get(JobParameterDTO.END_DATE);

            Map<Long, SaleForecast> saleForecastMap = saleForecastService.listByDateRange(property.getId(),
                            startDateTimestamp, endDateTimestamp).stream()
                    .collect(toMap(SaleForecast::getTheDate, Function.identity()));

            // 累积没有创建过的 Sale Forecast 信息，创建并进行邮件通知
            List<SaleForecast> newSaleForecasts = Lists.newArrayList();
            ZonedDateTime startZonedDateTime = DateTimeUtils.toZonedDateTime(startDateTimestamp, property.getTimezone());
            ZonedDateTime endZonedDateTime = DateTimeUtils.toZonedDateTime(endDateTimestamp, property.getTimezone());

            long nowTimestamp = DateTimeUtils.nowTimestamp();
            while (!startZonedDateTime.isAfter(endZonedDateTime)) {
                long theDate = startZonedDateTime.toEpochSecond();
                SaleForecast saleForecast = saleForecastMap.get(theDate);
                if (Objects.isNull(saleForecast)) {
                    SaleForecast newSaleForecast = new SaleForecast();
                    newSaleForecast.setPropertyId(property.getId());
                    newSaleForecast.setTheDate(theDate);
                    newSaleForecast.setNotWriteInTime(true);
                    newSaleForecast.setCreatedBy(BaseConstant.ZERO);
                    newSaleForecast.setCreatedTime(nowTimestamp);

                    newSaleForecasts.add(newSaleForecast);
                }
                startZonedDateTime = startZonedDateTime.plusDays(1L);
            }
            return newSaleForecasts;
        }
    }