# SiteDataTask.java

    package cloud.wconnect.frontoffice.businesstask.clc;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.FunctionCons;
    import cloud.wconnect.commonlib.pojo.dto.NotifyDTO;
    import cloud.wconnect.commonlib.pojo.dto.NotifyIdentityDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.line.PublicFlexMsgParm;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.MailInternalService;
    import cloud.wconnect.commonlib.service.NotifyInternalService;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.frontoffice.cons.FunctionPointEnum;
    import cloud.wconnect.frontoffice.cons.clc.CLCLineNotifyTitleEnum;
    import cloud.wconnect.frontoffice.cons.clc.CLCNotifyEnum;
    import cloud.wconnect.frontoffice.entity.clc.ClcEmailConfig;
    import cloud.wconnect.frontoffice.entity.clc.ClcEmailTarget;
    import cloud.wconnect.frontoffice.entity.clc.ClcGuestInh;
    import cloud.wconnect.frontoffice.entity.clc.ClcSiteData;
    import cloud.wconnect.frontoffice.pojo.vo.clc.ClcSiteDataVO;
    import cloud.wconnect.frontoffice.pojo.vo.clc.NoticeCheckOutVO;
    import cloud.wconnect.frontoffice.service.clc.IClcEmailConfigService;
    import cloud.wconnect.frontoffice.service.clc.IClcEmailTargetService;
    import cloud.wconnect.frontoffice.service.clc.IClcGuestInhService;
    import cloud.wconnect.frontoffice.service.clc.IClcSiteDataService;
    import com.alibaba.fastjson.JSONObject;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Sets;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Component;
    import org.thymeleaf.TemplateEngine;
    import org.thymeleaf.context.Context;

    import java.util.*;
    import java.util.stream.Collectors;

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

    /**
     * Site Data
     *
     * @author yyt
     * 2023/7/31
     */
    @Slf4j
    @Component
    public class SiteDataTask {

        @Autowired
        private IClcEmailConfigService clcEmailConfigService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private IClcEmailTargetService clcEmailTargetService;
        @Autowired
        private MailInternalService mailInternalService;
        @Autowired
        private TemplateEngine templateEngine;
        @Autowired
        private IClcGuestInhService clcGuestInhService;
        @Autowired
        private IClcSiteDataService clcSiteDataService;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private LineMessageUtil lineMessageUtil;
        @Autowired
        private NotifyInternalService notifyInternalService;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;

        @Value("${here.link.clc-missing-list}")
        private String hereLink;

        /**
         * 通知酒店处理 clc 平台顾客退房
         *
         * @param propertyId 资产 id
         * @author yyt
         * @date 2023/7/31
         */
        @Async
        public void notifyHandleCheckOut(Integer propertyId) {
            log.info("notifyHandleCheckOut... start");
            String alertTitle = "[Warning] - Exceptions About Auto Notify Handle Check Out";
            StringBuilder warningEmail = new StringBuilder("<p>There is something wrong when auto notify handle check out.</p>");

            try {
                // 查询邮件配置
                ClcEmailConfig config = clcEmailConfigService.getOne(new QueryWrapper<ClcEmailConfig>()
                        .lambda().eq(ClcEmailConfig::getType, ClcEmailConfig.TYPE_NOTIFY_HANDLE_CHECK_OUT)
                        .eq(ClcEmailConfig::getPropertyId, propertyId)
                        .eq(ClcEmailConfig::getStatus, true));
                if (Objects.isNull(config)) {
                    log.warn("notifyHandleCheckOut... no enabled mail config was found");
                    log.info("notifyHandleCheckOut... end");
                    return;
                }

                // 过滤有效酒店
                PropertyDTO property = propertyI18nRedisService.get(propertyId);
                if (!property.getStatus()) {
                    log.warn("notifyHandleCheckOut... the property is closed, no processing required");
                    log.info("notifyHandleCheckOut... end");
                    return;
                }

                List<ClcEmailTarget> emailTargets = clcEmailTargetService.list(new QueryWrapper<ClcEmailTarget>().lambda()
                        .eq(ClcEmailTarget::getConfigId, config.getId()));
                if (emailTargets.isEmpty()) {
                    log.warn("notifyHandleCheckOut... email target is not set");
                    log.info("notifyHandleCheckOut... end");
                    return;
                }

                // 查询目前指定酒店下的客人在店信息
                List<ClcGuestInh> checkInList = clcGuestInhService.list(new QueryWrapper<ClcGuestInh>().lambda()
                        .eq(ClcGuestInh::getPropertyId, propertyId));

                // 获取匹配 arrivals 记录，且没有 checkout 的 CLC 记录
                List<ClcSiteData> matchAndNotCheckOut = clcSiteDataService.listMatchAndNotCheckOut(propertyId);
                // 比对现有的客人在店信息，筛选出顾客已经离店，但未在 clc 平台处理退房的 clc 信息
                List<ClcSiteDataVO> alreadyCheckOutData = listAlreadyCheckOutData(matchAndNotCheckOut, checkInList, property);

                // 获取未匹配 arrivals 记录且没有 checkout 的 CLC 记录
                LambdaQueryWrapper<ClcSiteData> queryWrapper = new QueryWrapper<ClcSiteData>().lambda()
                        .eq(ClcSiteData::getPropertyId, propertyId)
                        .eq(ClcSiteData::getClickCheckOut, ClcSiteData.CHECK_OUT_NO);
                if (!matchAndNotCheckOut.isEmpty()) {
                    queryWrapper.notIn(ClcSiteData::getId, matchAndNotCheckOut.stream().map(ClcSiteData::getId).collect(toList()));
                }
                List<ClcSiteData> notMatchAndNotCheckOut = clcSiteDataService.list(queryWrapper);
                // 判断 checkout 日期是否超过当前日期，筛选出没有按时 checkout 的 CLC 记录信息
                List<ClcSiteDataVO> checkOutTransfiniteData = listCheckOutTransfiniteData(notMatchAndNotCheckOut, property);

                // 对比现有的客人在店信息，筛选出提前 checkout 的 CLC 记录信息
                List<ClcSiteDataVO> earlyCheckOutClcData = listEarlyCheckOutClcDataByPropertyId(checkInList, property);

                if (alreadyCheckOutData.isEmpty() && checkOutTransfiniteData.isEmpty() && earlyCheckOutClcData.isEmpty()) {
                    log.info("notifyHandleCheckOut... the property guests not check out,so not need handle.");
                    log.info("notifyHandleCheckOut... end");
                    return;
                }

                NoticeCheckOutVO noticeCheckOutVO = new NoticeCheckOutVO();
                noticeCheckOutVO.setPropertyName(property.getName());
                noticeCheckOutVO.setAlreadyCheckOutData(alreadyCheckOutData);
                noticeCheckOutVO.setCheckOutTransfiniteData(checkOutTransfiniteData);
                noticeCheckOutVO.setEarlyCheckOutData(earlyCheckOutClcData);

                //发送通知
                doNotifyHandleCheckOut(property, config, emailTargets, noticeCheckOutVO);

                log.info("notifyHandleCheckOut... send the email request; property:{}", property.getAbbr());
            } catch (Exception e) {
                log.warn("notifyHandleCheckOut... exception occurs:", e);
                warningEmail.append("<p>Exception:").append(e).append("</p>");
                mailInternalService.sendAlertEmail(alertTitle, warningEmail.toString());
            }

            log.info("notifyHandleCheckOut... end");
        }

        /**
         * 发送通知
         *
         * @param propertyDTO      资产DTO
         * @param clcEmailConfig   邮件配置
         * @param emailTargets     邮件通知人集合
         * @param noticeCheckOutVO CLC退房的数据
         * @author lichen
         * @date 2023/12/14
         */
        private void doNotifyHandleCheckOut(PropertyDTO propertyDTO, ClcEmailConfig clcEmailConfig,
                                            List<ClcEmailTarget> emailTargets, NoticeCheckOutVO noticeCheckOutVO) {
            Integer propertyId = propertyDTO.getId();
            String propertyName = propertyDTO.getName();
            String language = propertyDTO.getLanguage();

            String finalHereLink = hereLink + "?property=" + propertyDTO.getAbbr() + "&tenant=" + propertyDTO.getTenantSymbol();
            String defaultSubject = clcEmailConfigService.getDefaultSubject(propertyId, CLCNotifyEnum.NO_CLICK_CHECK_OUT.getCode());

            //邮件内容
            Context context = new Context();
            context.setVariable("propertyName", propertyName);
            context.setVariable("functionName", i18nMessageUtils.message(FunctionCons.CLC_LIST, language));
            context.setVariable("noticeCheckOutVO", noticeCheckOutVO);
            context.setVariable("hereLink", finalHereLink);
            String emailContent = templateEngine.process("clc/clc-handle-check-out_" + language, context);
            emailContent = EmailUtil.getFullEmailContent(defaultSubject, emailContent, language);

            //line通知内容
            LinkedHashMap<String, String> businessMap = new LinkedHashMap<>();
            int total = 0;
            if (CollectionUtils.isNotEmpty(noticeCheckOutVO.getAlreadyCheckOutData())) {
                total += noticeCheckOutVO.getAlreadyCheckOutData().size();
            }
            if (CollectionUtils.isNotEmpty(noticeCheckOutVO.getCheckOutTransfiniteData())) {
                total += noticeCheckOutVO.getCheckOutTransfiniteData().size();
            }
            if (CollectionUtils.isNotEmpty(noticeCheckOutVO.getEarlyCheckOutData())) {
                total += noticeCheckOutVO.getEarlyCheckOutData().size();
            }
            businessMap.put("Total number of abnormal CLC records", total + " pieces");

            PublicFlexMsgParm publicFlexMsgParm = PublicFlexMsgParm.builder()
                    .propertyId(propertyId)
                    .propertyName(propertyName)
                    .functionName(i18nMessageUtils.message(FunctionCons.CLC_LIST, language))
                    .title(CLCLineNotifyTitleEnum.NO_CLICK_CHECK_OUT.getI18nCode())
                    .link(finalHereLink)
                    .businessMap(businessMap)
                    .build();
            JSONObject lineContent = lineMessageUtil.generatePublicFlexMsg(publicFlexMsgParm);

            NotifyDTO notifyDTO = new NotifyDTO();
            notifyDTO.setPropertyId(propertyId);
            notifyDTO.setFunctionPoint(FunctionPointEnum.CLC_CHECK_OUT.getFunctionPoint());
            notifyDTO.setSubject(StringUtils.isNotBlank(clcEmailConfig.getSubject()) ? clcEmailConfig.getSubject() : defaultSubject);
            notifyDTO.setIdentityList(beanMapper.mapList(emailTargets, NotifyIdentityDTO.class));
            notifyDTO.setEmailContent(emailContent);
            notifyDTO.setLineContent(Lists.newArrayList(lineContent));

            //通知
            notifyInternalService.sendNotify(notifyDTO);
        }

        /**
         * 筛选出顾客已经离店 未及时退房的信息
         *
         * @param noCheckOut  未退房 clc 数据
         * @param checkInList 入住人员信息
         * @param propertyDTO 资产
         * @return 未及时退房的信息
         * @author yyt
         * @date 2023/7/31
         */
        private List<ClcSiteDataVO> listAlreadyCheckOutData(List<ClcSiteData> noCheckOut, List<ClcGuestInh> checkInList,
                                                            PropertyDTO propertyDTO) {
            if (CollectionUtils.isEmpty(noCheckOut)) {
                return Collections.emptyList();
            }
            // 汇总入住信息
            Map<Integer, Set<String>> inHouseKeysByPropertyId = checkInList.stream()
                    .collect(Collectors.groupingBy(ClcGuestInh::getPropertyId,
                            Collectors.mapping(ClcGuestInh::getUniKey, Collectors.toSet())));
            Map<Integer, Set<String>> nameReversalInHouseKeysByPropertyId = checkInList.stream()
                    .collect(Collectors.groupingBy(ClcGuestInh::getPropertyId,
                            Collectors.mapping(ClcGuestInh::getUniKeyForNameReversal, Collectors.toSet())));

            // 对比筛选出客人已经离店但仍未checkout的CLC记录
            Map<Integer, List<ClcSiteData>> noCheckoutInTimeDataByPropertyId = noCheckOut.stream().filter(item -> {
                Set<String> inHouseKeys = inHouseKeysByPropertyId.getOrDefault(item.getPropertyId(), Sets.newHashSet());
                inHouseKeys.addAll(nameReversalInHouseKeysByPropertyId.getOrDefault(item.getPropertyId(), Sets.newHashSet()));
                if (CollectionUtils.isEmpty(inHouseKeys)) {
                    return true;
                }

                return !inHouseKeys.contains(item.getUniKey());
            }).collect(Collectors.groupingBy(ClcSiteData::getPropertyId));
            if (noCheckoutInTimeDataByPropertyId.isEmpty()) {
                return Collections.emptyList();
            }

            return handleClcSiteData(noCheckoutInTimeDataByPropertyId.get(propertyDTO.getId()), propertyDTO);
        }

        /**
         * 筛选出已经超过 check out 未及时退房的信息
         *
         * @param notMatchAndNotCheckOut 未退房的 clc 信息
         * @param propertyDTO            资产
         * @return 未及时退房的信息
         * @author yyt
         * @date 2023/7/31
         */
        private List<ClcSiteDataVO> listCheckOutTransfiniteData(List<ClcSiteData> notMatchAndNotCheckOut,
                                                                PropertyDTO propertyDTO) {
            long date = DateTimeUtils.toTimestamp(DateTimeUtils.dateNow(propertyDTO.getTimezone()));
            List<ClcSiteData> noCheckoutInTime = notMatchAndNotCheckOut.stream()
                    .filter(item -> item.getCheckOutDate() <= date).collect(Collectors.toList());

            return handleClcSiteData(noCheckoutInTime, propertyDTO);
        }

        /**
         * 刷选出提前退房的信息
         *
         * @param checkInList 当前入住信息
         * @param propertyDTO 资产
         * @return 提前退房的信息
         * @author yyt
         * @date 2023/7/31
         */
        private List<ClcSiteDataVO> listEarlyCheckOutClcDataByPropertyId(List<ClcGuestInh> checkInList,
                                                                         PropertyDTO propertyDTO) {
            if (CollectionUtils.isEmpty(checkInList)) {
                return Collections.emptyList();
            }

            List<ClcSiteData> checkOutData = clcSiteDataService.listMatchAndCheckOut(checkInList);
            if (CollectionUtils.isEmpty(checkOutData)) {
                return Collections.emptyList();
            }

            // 获取 folio number 关联未退房的信息
            List<String> folioNumbers = checkOutData.stream().map(ClcSiteData::getFolioNumber).collect(toList());
            Map<String, Long> noCheckoutCountByFolioNumber;
            if (CollectionUtils.isNotEmpty(folioNumbers)) {
                noCheckoutCountByFolioNumber = clcSiteDataService.list(new QueryWrapper<ClcSiteData>().lambda()
                                .eq(ClcSiteData::getClickCheckOut, ClcSiteData.CHECK_OUT_NO)
                                .in(ClcSiteData::getFolioNumber, folioNumbers)).stream()
                        .collect(Collectors.groupingBy(ClcSiteData::getFolioNumber, Collectors.counting()));
            } else {
                noCheckoutCountByFolioNumber = Collections.emptyMap();
            }

            // 已退房信息以 folio number 分组
            Map<String, List<ClcSiteData>> earlyCheckOutsByFolioNumber = checkOutData.stream()
                    .collect(Collectors.groupingBy(ClcSiteData::getFolioNumber));
            // 筛选相同 folio number 下，有没有未退房的信息，如果都是已退房，则表示是提前退房
            List<ClcSiteDataVO> clcSiteDataVOs = Lists.newArrayListWithExpectedSize(earlyCheckOutsByFolioNumber.values().size());
            earlyCheckOutsByFolioNumber.forEach((folioNumber, earlyCheckOuts) -> {
                long noCheckOutCount = noCheckoutCountByFolioNumber.getOrDefault(folioNumber, 0L);
                if (noCheckOutCount == 0) {
                    clcSiteDataVOs.addAll(handleClcSiteData(earlyCheckOuts, propertyDTO));
                }
            });

            return clcSiteDataVOs;
        }

        /**
         * 处理ClcSiteData转成ClcSiteDataVo
         *
         * @param clcSiteDataList clc平台数据
         * @param propertyDTO     资产
         * @return clcSiteDataVO集合
         * @author yyt
         * 2020/8/20
         */
        public List<ClcSiteDataVO> handleClcSiteData(List<ClcSiteData> clcSiteDataList, PropertyDTO propertyDTO) {
            List<ClcSiteDataVO> siteDataVos = Lists.newArrayListWithExpectedSize(clcSiteDataList.size());
            String timezone = propertyDTO.getTimezone();
            String language = propertyDTO.getLanguage();
            for (ClcSiteData clcSiteData : clcSiteDataList) {
                ClcSiteDataVO siteDataVo = beanMapper.map(clcSiteData, ClcSiteDataVO.class);
                siteDataVo.setCheckInDate(InternationalizationUtils.formatDateTime(clcSiteData.getCheckInDate(),
                        propertyDTO.getInternationalization().getDateTime().getLongDate(), StringUtils.EMPTY, timezone, language));
                siteDataVo.setCheckOutDate(InternationalizationUtils.formatDateTime(clcSiteData.getCheckOutDate(),
                        propertyDTO.getInternationalization().getDateTime().getLongDate(), StringUtils.EMPTY, timezone, language));

                if (StringUtils.isEmpty(siteDataVo.getFolioNumber())) {
                    siteDataVo.setFolioNumber("--");
                }
                if (StringUtils.isEmpty(siteDataVo.getRoomNumber())) {
                    siteDataVo.setRoomNumber("--");
                }

                siteDataVos.add(siteDataVo);
            }
            return siteDataVos;
        }

    }