# ValidationLayoutTask.java

    package cloud.wconnect.finance.businesstask.pl;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.FunctionIdConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.MailInfoDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.m3Config.PropertyM3ConfigDTO;
    import cloud.wconnect.commonlib.pojo.dto.m3Config.TenantM3ConfigDTO;
    import cloud.wconnect.commonlib.pojo.query.OpenFunctionPropertyQuery;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.M3ConfigInternalService;
    import cloud.wconnect.commonlib.service.MailInternalService;
    import cloud.wconnect.commonlib.service.PropertyInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.finance.entity.pl.M3Account;
    import cloud.wconnect.finance.entity.pl.M3Row;
    import cloud.wconnect.finance.entity.pl.M3Trialbalance;
    import cloud.wconnect.finance.entity.pl.PlLayoutValidation;
    import cloud.wconnect.finance.pojo.dto.pl.m3api.CustomerCloseAccountDTO;
    import cloud.wconnect.finance.service.pl.*;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    import org.thymeleaf.TemplateEngine;
    import org.thymeleaf.context.Context;

    import java.math.BigDecimal;
    import java.text.MessageFormat;
    import java.time.Year;
    import java.time.YearMonth;
    import java.time.ZoneId;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;

    /**
     * Validation Layout.xls 定时任务
     *
     * @author lichen
     * @date 2026/3/16
     */
    @Slf4j
    @Component
    public class ValidationLayoutTask {

        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        @Value("${default.timezone}")
        private String defaultTimezone;
        @Autowired
        private MailInternalService mailInternalService;
        @Autowired
        private PropertyInternalService propertyInternalService;
        @Autowired
        private M3ConfigInternalService m3ConfigInternalService;
        @Autowired
        private IM3PlReportService m3PlReportService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private M3ApiService m3ApiService;
        @Autowired
        private IPlLayoutValidationService plLayoutValidationService;
        @Autowired
        private M3DataService m3DataService;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private IM3RowService m3RowService;
        @Autowired
        private IM3TrialbalanceService m3TrialbalanceService;
        @Autowired
        private IM3AccountService m3AccountService;
        @Autowired
        private TemplateEngine templateEngine;
        @Value("${w-connect.alert.pl-mails}")
        private String toEmails;
        // 定时任务执行并发前缀
        private static final String TASK_PREFIX_KEY = "wconnect:task:pl:validation-layout:";
        // 验证结果并发前缀
        private static final String VALIDATION_RESULT_KEY = "wconnect:task:pl:validation-layout-result:{0}:{1}";

        /**
         * 每日凌晨8点开始检查
         *
         * @author lichen
         * @date 2026/3/16
         */
        @Scheduled(cron = "${schedule.cron.pl.validation-layout}", zone = "${default.timezone}")
        public void validationLayout() {
            try {
                String currentDateStr = DateTimeUtils.dateNowStr(defaultTimezone, BaseConstant.DATE_FORMAT_YYMMDDHH);
                if (!preventConcurrency(TASK_PREFIX_KEY + currentDateStr)) {
                    return;
                }
                //查询开通P&L功能的有效资产
                List<PropertyDTO> openFunctionPropertyDTOList = propertyInternalService.getOpenFunctionProperty(OpenFunctionPropertyQuery.builder()
                                .openFunctionIds(Lists.newArrayList(FunctionIdConstant.PL)).build())
                        .stream().filter(PropertyDTO::getStatus).toList();
                if (CollectionUtils.isEmpty(openFunctionPropertyDTOList)) {
                    log.info("ValidationLayoutTask, no open function property");
                    return;
                }
                List<Integer> propertyIds = openFunctionPropertyDTOList.stream().map(PropertyDTO::getId).toList();

                //获取资产的M3配置
                List<TenantM3ConfigDTO> tenantM3ConfigDTOS = m3ConfigInternalService.listPropertyM3Config(propertyIds);
                if (CollectionUtils.isEmpty(tenantM3ConfigDTOS)) {
                    log.info("ValidationLayoutTask, no m3 config");
                    return;
                }

                //获取当前年yyyy
                int currentYear = Year.now().getValue();

                for (TenantM3ConfigDTO tenantM3ConfigDTO : tenantM3ConfigDTOS) {
                    List<PropertyM3ConfigDTO> propertyM3ConfigDTOS = tenantM3ConfigDTO.getPropertyM3ConfigDTOS();
                    if (CollectionUtils.isEmpty(propertyM3ConfigDTOS)) {
                        log.info("ValidationLayoutTask, no property m3 config, tenantId:" + tenantM3ConfigDTO.getTenantId());
                        continue;
                    }

                    //客户的关账信息
                    CustomerCloseAccountDTO customerCloseAccountDTO = m3ApiService.getCustomerCloseAccount(tenantM3ConfigDTO, Integer.toString(currentYear));

                    for (PropertyM3ConfigDTO propertyM3ConfigDTO : propertyM3ConfigDTOS) {
                        //执行校验逻辑
                        doValidationLayout(propertyM3ConfigDTO, customerCloseAccountDTO);
                    }
                }
            } catch (Exception e) {
                String alertTitle = "[Warning] - P&L Exceptions about validation layout";
                //发送预警邮件
                mailInternalService.sendAlertEmail(alertTitle,
                        "<p>There is something wrong when validation layout.</p>"
                                + "<p>Exception:" + e + "</p>"
                );
            }
        }

        /**
         * 验证Layout
         *
         * @param propertyM3ConfigDTO  资产M3配置
         * @param customerCloseAccount 客户关账信息
         * @author lichen
         * @date 2026/3/16
         */
        private void doValidationLayout(PropertyM3ConfigDTO propertyM3ConfigDTO, CustomerCloseAccountDTO customerCloseAccount) {
            try {
                PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyM3ConfigDTO.getPropertyId());
                ZoneId zoneId = ZoneId.of(propertyDTO.getTimezone());

                //获取上个月的时间，格式yyyyMM
                YearMonth lastMonth = YearMonth.now(zoneId).minusMonths(1);
                Integer lastMonthInteger = Integer.valueOf(YearMonthUtils.DATE_TIME_FORMATTER.format(lastMonth));

                //判断客户是否已经验证
                String key = MessageFormat.format(VALIDATION_RESULT_KEY, propertyDTO.getId(), lastMonthInteger.toString());
                if (redisTemplate.hasKey(key)) {
                    log.info("ValidationLayoutTask, property has been validated, propertyId:" + propertyDTO.getId() + ", month:" + lastMonthInteger);
                    return;
                }

                //判断客户是否关账
                boolean closed = m3DataService.isClosed(customerCloseAccount, propertyM3ConfigDTO, lastMonthInteger);
                if (!closed) {
                    log.info("ValidationLayoutTask, property has been closed, propertyId:" + propertyDTO.getId() + ", month:" + lastMonthInteger);
                    return;
                }

                // validations的配置
                List<PlLayoutValidation> plLayoutValidations = plLayoutValidationService.getByPropertyId(propertyDTO.getId());
                Map<Integer, String> type2Variables = plLayoutValidations.stream().collect(Collectors.toMap(PlLayoutValidation::getType, PlLayoutValidation::getVariables));
                Set<Integer> typeList = type2Variables.keySet();
                if (CollectionUtils.isEmpty(typeList) || !typeList.contains(PlLayoutValidation.TYPE_GL_ACCOUNT_PTD_SUM)
                        || !typeList.contains(PlLayoutValidation.TYPE_ROW_PTD_SUM)) {
                    throw new RuntimeException("PL Layout Validation Config not exist");
                }

                List<String> rowNumberList = JsonUtils.convertStringToList(objectMapper, type2Variables.get(PlLayoutValidation.TYPE_ROW_PTD_SUM), String.class);
                List<M3Row> m3RowList = m3RowService.listByRowNumber(propertyDTO.getId(), rowNumberList);
                if (CollectionUtils.isEmpty(m3RowList) || m3RowList.size() != rowNumberList.size()) {
                    throw new RuntimeException("PL Layout Validation Config: [CASH FROM OPERATIONS] + [Division Income/Loss - Tenant] + [Income/Loss - F&B II] not exist");
                }

                List<M3Account> m3Accounts = m3AccountService.listByPropertyId(propertyDTO.getId());
                List<M3Trialbalance> m3Trialbalances = m3TrialbalanceService.listByYearMonth(propertyDTO.getId(), lastMonthInteger);

                // 待计算的 Account
                List<M3Account> needCalculateM3Accounts = plLayoutValidationService.getGroupAccount(propertyDTO.getId(), m3Accounts,
                        type2Variables.get(PlLayoutValidation.TYPE_GL_ACCOUNT_PTD_SUM));
                // 计算一组Account PTD的和
                BigDecimal accountPTDSum = m3TrialbalanceService.calculateAccountPTDSum(propertyDTO.getId(), m3Trialbalances, needCalculateM3Accounts);
                // 计算一组Row PTD的和
                BigDecimal rowPTDSum = m3PlReportService.calculateRowPTDSum(propertyDTO.getId(), lastMonthInteger, m3RowList, m3Accounts, m3Trialbalances);

                // 计算两个bigDecimal的绝对值是否相等
                if(accountPTDSum.abs().compareTo(rowPTDSum.abs()) != 0){
                    Context context = new Context();
                    context.setVariable("propertyName", propertyDTO.getName());
                    context.setVariable("accountPTDSum", accountPTDSum.stripTrailingZeros().toPlainString());
                    context.setVariable("rowPTDSum", rowPTDSum.stripTrailingZeros().toPlainString());
                    context.setVariable("contractEmail", "wconnect.tw@friendwell.com");
                    context.setVariable("copyrightYear", String.valueOf(Year.now().getValue()));
                    context.setVariable("yearMonth", YearMonthUtils.parseYearMonth(lastMonth, BaseConstant.DATE_FORMAT_YYYY_MM));
                    String mailContent = templateEngine.process("pl/layout-validation.html", context);

                    MailInfoDTO mailInfoDTO = new MailInfoDTO();
                    mailInfoDTO.setSubject("IWC P&L Validation Alert")
                            .setContent(mailContent)
                            .setToEmails(toEmails);
                    // 发送邮件
                    mailInternalService.sendEmail(mailInfoDTO);

                    // 设置缓存
                    redisTemplate.opsForValue().set(key, 1, 5, TimeUnit.DAYS);
                }
            } catch (BadRequestException exception) {
                log.warn("GeneratePLReportTask.doGeneratePLReport check error, propertyId:" + propertyM3ConfigDTO.getPropertyId(), exception);
            } catch (Exception e) {
                String alertTitle = "[Warning] - P&L Exceptions about generate PL Report of propertyId:" + propertyM3ConfigDTO.getPropertyId();
                //发送预警邮件
                mailInternalService.sendAlertEmail(alertTitle,
                        "<p>There is something wrong when generate PL Report of propertyId: " + propertyM3ConfigDTO.getPropertyId() + ".</p>"
                                + "<p>Exception:" + e + "</p>"
                );
            }
        }

        /**
         * 阻止并发操作
         *
         * @param key 缓存key
         * @return java.lang.Boolean
         * @author lichen
         * @date 2026/3/16
         */
        private Boolean preventConcurrency(String key) {
            return redisTemplate.opsForValue().setIfAbsent(key, 0, 1, TimeUnit.DAYS);
        }
    }