# HistoryAndForecastListenerFileParseByOperaImpl.java

    package cloud.wconnect.rateshop.service.revenue.historyandforecast;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.SystemPlatformEnum;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.RoomsConfigDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.RoomsConfigInternalService;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.rateshop.cons.revenue.RevenueCons;
    import cloud.wconnect.rateshop.entity.revenue.RevenueAdrConfig;
    import cloud.wconnect.rateshop.pojo.bo.revenue.HistoryAndForecastBO;
    import cloud.wconnect.rateshop.pojo.bo.revenue.OtbAndTransientGroupBO;
    import cloud.wconnect.rateshop.pojo.bo.revenue.OtbRevenueBO;
    import cloud.wconnect.rateshop.pojo.bo.revenue.TransientGroupBO;
    import cloud.wconnect.rateshop.service.revenue.IRevenueAdrConfigService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.google.common.collect.Lists;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.dom4j.Document;
    import org.dom4j.Element;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    import java.io.InputStream;
    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.LocalDate;
    import java.time.ZoneId;
    import java.time.ZonedDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.Collections;
    import java.util.List;
    import java.util.Objects;

    /**
     * @author yyt
     * 2023/11/1
     */
    @Service
    @DS("rate_shop")
    public class HistoryAndForecastListenerFileParseByOperaImpl implements IHistoryAndForecastListenerFileParse {

        @Autowired
        private RoomsConfigInternalService roomsConfigInternalService;
        @Autowired
        private IRevenueAdrConfigService revenueAdrConfigService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;

        private static final String SOURCE = SystemPlatformEnum.OPERA.getDesc();

        /**
         * 判断支持的平台
         *
         * @param source 平台
         * @return boolean
         * @author yyt
         * @date 2023/11/1
         */
        @Override
        public boolean supports(String source) {
            return SOURCE.equals(source);
        }

        /**
         * 解析报告数据
         *
         * @param propertyId 资产 id
         * @param timeZone   时区
         * @param in         文件流
         * @param startDate  开始日期
         * @param endDate    结束日期
         * @return Otb And TransientGroup
         * @author yyt
         * @date 2023/11/1
         */
        @Override
        public OtbAndTransientGroupBO parseReport(Integer propertyId, String timeZone, InputStream in, Long startDate, Long endDate) throws Exception {
            // 解析文件数据
            Document document = new SAXReader().read(in);
            OtbRevenueBO otbRevenueBO = parseDocumentForOtb(propertyId, document);
            List<TransientGroupBO> transientGroups = parseDocumentForTransientGroup(propertyId, timeZone,
                    document, startDate, endDate);
            return new OtbAndTransientGroupBO(otbRevenueBO, transientGroups);
        }

        /**
         * 解析History and Forecast from thirty days ago to yesterday报告数据
         *
         * @param propertyDTO 资产
         * @param in          文件流
         * @param startDate   开始日期
         * @param endDate     结束日期
         * @return java.util.List<cloud.wconnect.rateshop.pojo.bo.revenue.HistoryAndForecastBO>
         * @author lichen
         * @date 2025/8/5
         */
        @Override
        public List<HistoryAndForecastBO> parseReportForTransientGroupLastYear(PropertyDTO propertyDTO, InputStream in,
                                                                               Long startDate, Long endDate) throws Exception {
            // 解析文件数据
            Document document = new SAXReader().read(in);
            return parseDocumentForTransientGroupLastYear(propertyDTO, document, startDate, endDate);
        }

        /**
         * 解析 Otb 数据
         *
         * @param propertyId 资产 id
         * @param document   文件
         * @return otb revenue
         * @author yyt
         * @date 2023/11/1
         */
        private OtbRevenueBO parseDocumentForOtb(Integer propertyId, Document document) {
            OtbRevenueBO otbRevenue = new OtbRevenueBO();
            // 读取 Room Revenue
            Node node = document.selectSingleNode("//SUMREVENUEPERREPORT");
            if (Objects.nonNull(node)) {
                Element dataElement = (Element) node;
                String roomRevenueStr = dataElement.getTextTrim();
                if (StringUtils.isNotEmpty(roomRevenueStr)) {
                    otbRevenue.setOtbRevenue(new BigDecimal(roomRevenueStr));
                }
            }

            // 读取 Average Rate（ADR）
            node = document.selectSingleNode("//CF_AVERAGE_ROOM_RATE_REPORT");
            if (Objects.nonNull(node)) {
                Element dataElement = (Element) node;
                String adrStr = dataElement.getTextTrim();
                if (StringUtils.isNotEmpty(adrStr)) {
                    otbRevenue.setOtbAdr(new BigDecimal(adrStr));
                }
            }

            InternationalizationNumberDTO number = propertyI18nRedisService.get(propertyId).getInternationalization().getNumber();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());
            int dataPrecision = BaseConstant.DATA_PRECISION;

            // 如果配置了房间数，则根据 occ 计算规则判断：1.使用配置的房间数计算 occ，2.直接使用文件中的 occ
            RoomsConfigDTO roomsConfig = roomsConfigInternalService.getRoomsConfig(propertyId);
            if (roomsConfig.getOccRule() == RevenueCons.USE_ROOMS_TO_CALCULATE) {
                // 读取 Total Occ.
                node = document.selectSingleNode("//SUMCALC_OCCROOMSPERREPORT");
                if (Objects.nonNull(node)) {
                    Element dataElement = (Element) node;
                    String totalOccRoomsStr = dataElement.getTextTrim();
                    if (StringUtils.isNotEmpty(totalOccRoomsStr)) {
                        // 读取总天数
                        List<Node> nodes = document.selectNodes("//G_CONSIDERED_DATE");
                        if (!nodes.isEmpty()) {
                            //  Occ. % = Total Occ / (房间数 * 总天数)
                            int totalDays = nodes.size();
                            BigDecimal occPercent = new BigDecimal(totalOccRoomsStr)
                                    .divide(BigDecimal.valueOf((long) roomsConfig.getRooms() * totalDays), dataPrecision, roundingMode)
                                    .multiply(BigDecimal.valueOf(100L));
                            otbRevenue.setOtbOccPercent(occPercent);
                        }
                    }
                }
            } else {
                // 读取 Occ%
                node = document.selectSingleNode("//CF_OCCUPANCY_REPORT");
                if (Objects.nonNull(node)) {
                    Element dataElement = (Element) node;
                    String occPercentStr = dataElement.getTextTrim();
                    if (StringUtils.isNotEmpty(occPercentStr)) {
                        otbRevenue.setOtbOccPercent(new BigDecimal(occPercentStr).setScale(dataPrecision, roundingMode));
                    }
                }
            }
            return otbRevenue;
        }

        /**
         * 解析 Transient Group 数据
         *
         * @param propertyId 资产 id
         * @param timeZone   时区
         * @param document   文件
         * @param startDate  开始日期
         * @param endDate    结束日期
         * @return Transient And Group
         * @author yyt
         * @date 2023/10/30
         */
        private List<TransientGroupBO> parseDocumentForTransientGroup(Integer propertyId, String timeZone, Document document, Long startDate, Long endDate) {
            List<Node> nodes = document.selectNodes("//G_CONSIDERED_DATE");
            if (CollectionUtils.isEmpty(nodes)) {
                return Collections.emptyList();
            }

            RoomsConfigDTO roomsConfig = roomsConfigInternalService.getRoomsConfig(propertyId);
            Integer rooms = roomsConfig.getRooms();

            RevenueAdrConfig revenueAdrConfig = revenueAdrConfigService.get(propertyId);
            boolean checkTransientDeduct = Objects.nonNull(revenueAdrConfig) && revenueAdrConfig.getCheckTransientDeduct();
            boolean checkTransientNonDeduct = Objects.nonNull(revenueAdrConfig) && revenueAdrConfig.getCheckTransientNonDeduct();
            boolean checkGroupDeduct = Objects.nonNull(revenueAdrConfig) && revenueAdrConfig.getCheckGroupDeduct();
            boolean checkGroupNonDeduct = Objects.nonNull(revenueAdrConfig) && revenueAdrConfig.getCheckGroupNonDeduct();

            // 最多1个月的数据
            List<TransientGroupBO> transientGroups = Lists.newArrayListWithExpectedSize(31);
            ZoneId zoneId = ZoneId.of(timeZone);
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_MM_DD_YY_STRIKE);
            DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYY_MM_DD);
            BigDecimal oneHundred = BigDecimal.valueOf(100L);
            InternationalizationNumberDTO number = propertyI18nRedisService.get(propertyId).getInternationalization().getNumber();
            int dataPrecision = BaseConstant.DATA_PRECISION;
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());
            for (Node node : nodes) {
                Element dataElement = (Element) node;
                String theDateStr = dataElement.element("CHAR_CONSIDERED_DATE").getTextTrim();
                if (!theDateStr.isEmpty()) {
                    ZonedDateTime targetDateZoneDateTime = LocalDate.parse(StringUtils.substringBefore(theDateStr, " "),
                            dateTimeFormatter).atStartOfDay(zoneId);
                    long targetDate = targetDateZoneDateTime.toEpochSecond();
                    if (targetDate >= startDate && targetDate <= endDate) {
                        TransientGroupBO transientGroup = new TransientGroupBO();
                        transientGroup.setTargetDate(targetDateZoneDateTime.format(dateTimeFormatter2));

                        // 如果配置了房间数，则根据 occ 计算规则判断：1.使用配置的房间数计算 occ，2.直接使用文件中的 occ
                        if (roomsConfig.getOccRule() == RevenueCons.USE_ROOMS_TO_CALCULATE) {
                            String totalOccRoomsStr = dataElement.element("CF_CALC_OCC_ROOMS").getTextTrim();
                            if (StringUtils.isNotEmpty(totalOccRoomsStr)) {
                                //  Occ. % = total occ rooms / 总房间数
                                BigDecimal occPercent = new BigDecimal(totalOccRoomsStr).divide(BigDecimal.valueOf(rooms),
                                        dataPrecision, roundingMode).multiply(oneHundred);
                                transientGroup.setOccPercent(occPercent);
                            }
                        } else {
                            String occPercentStr = dataElement.element("CF_OCCUPANCY").getTextTrim();
                            if (StringUtils.isNotEmpty(occPercentStr)) {
                                transientGroup.setOccPercent(new BigDecimal(occPercentStr).setScale(dataPrecision, roundingMode));
                            }
                        }

                        String adrStr = dataElement.element("CF_AVERAGE_ROOM_RATE").getTextTrim();
                        if (StringUtils.isNotEmpty(adrStr)) {
                            transientGroup.setAdr(new BigDecimal(adrStr).setScale(dataPrecision, roundingMode));
                        }

                        // 如果勾选了 Transient deduct，则累加交订金的 Transient 房间数
                        String transientRoomsStr = dataElement.element("IND_DEDUCT_ROOMS").getTextTrim();
                        if (checkTransientDeduct && StringUtils.isNotEmpty(transientRoomsStr)) {
                            transientGroup.setTransientRoomNum(Integer.valueOf(transientRoomsStr));
                        }

                        // 如果勾选了 Transient non deduct，则累加未交订金的 Transient 房间数
                        String nonDeductRoomsStr = dataElement.element("IND_NON_DEDUCT_ROOMS").getTextTrim();
                        if (checkTransientNonDeduct && StringUtils.isNotEmpty(nonDeductRoomsStr)) {
                            Integer transientRoomNum = transientGroup.getTransientRoomNum();
                            Integer nonDeductRooms = Integer.valueOf(nonDeductRoomsStr);
                            if (transientRoomNum == null) {
                                transientGroup.setTransientRoomNum(nonDeductRooms);
                            } else {
                                transientGroup.setTransientRoomNum(transientRoomNum + nonDeductRooms);
                            }
                        }

                        // 如果勾选了 Group deduct，则累加交订金的 Group 房间数
                        String groupRoomsStr = dataElement.element("GRP_DEDUCT_ROOMS").getTextTrim();
                        if (checkGroupDeduct && StringUtils.isNotEmpty(groupRoomsStr)) {
                            transientGroup.setGroupRoomNum(Integer.valueOf(groupRoomsStr));
                        }

                        // 如果勾选了 Group non deduct，则累加未交订金的 Group 房间数
                        String grpNonDeductRoomsStr = dataElement.element("GRP_NON_DEDUCT_ROOMS").getTextTrim();
                        if (checkGroupNonDeduct && StringUtils.isNotEmpty(grpNonDeductRoomsStr)) {
                            Integer groupRoomNum = transientGroup.getGroupRoomNum();
                            Integer grpNonDeductRooms = Integer.valueOf(grpNonDeductRoomsStr);
                            if (groupRoomNum == null) {
                                transientGroup.setGroupRoomNum(grpNonDeductRooms);
                            } else {
                                transientGroup.setGroupRoomNum(groupRoomNum + grpNonDeductRooms);
                            }
                        }

                        // 计算 Rooms Available = 酒店房间数 - Total Occ. - OOO Rooms
                        Integer roomsAvailable = rooms;
                        if (Objects.nonNull(roomsAvailable)) {
                            String totalOcc = dataElement.element("CF_CALC_OCC_ROOMS").getTextTrim();
                            String oooRooms = dataElement.element("CF_OOO_ROOMS").getTextTrim();
                            if (StringUtils.isNotEmpty(totalOcc)) {
                                roomsAvailable -= Integer.parseInt(totalOcc);
                            }
                            if (StringUtils.isNotEmpty(oooRooms)) {
                                roomsAvailable -= Integer.parseInt(oooRooms);
                            }
                        }
                        transientGroup.setRoomsAvailable(roomsAvailable);

                        transientGroups.add(transientGroup);

                        // 提前结束循环
                        if (targetDate == endDate) {
                            break;
                        }
                    }
                }
            }
            return transientGroups;
        }

        /**
         * 解析history and forecast报告数据
         *
         * @param propertyDTO 资产
         * @param document    文件
         * @param startDate   开始日期
         * @param endDate     结束日期
         * @return java.util.List<cloud.wconnect.rateshop.pojo.bo.revenue.HistoryAndForecastBO>
         * @author lichen
         * @date 2025/8/5
         */
        private List<HistoryAndForecastBO> parseDocumentForTransientGroupLastYear(PropertyDTO propertyDTO,
                                                                                  Document document, Long startDate, Long endDate) {
            List<Node> nodes = document.selectNodes("//G_CONSIDERED_DATE");
            if (CollectionUtils.isEmpty(nodes)) {
                return Collections.emptyList();
            }

            // 最多 104 天数据
            List<HistoryAndForecastBO> historyAndForecastBOS = Lists.newArrayListWithExpectedSize(31);
            ZoneId zoneId = ZoneId.of(propertyDTO.getTimezone());
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_MM_DD_YY_STRIKE);
            DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYY_MM_DD);
            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            int dataPrecision = BaseConstant.DATA_PRECISION;
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());

            for (Node node : nodes) {
                Element dataElement = (Element) node;
                String theDateStr = dataElement.element("CHAR_CONSIDERED_DATE").getTextTrim();
                if (!theDateStr.isEmpty()) {
                    ZonedDateTime targetDateZoneDateTime = LocalDate.parse(StringUtils.substringBefore(theDateStr, " "),
                            dateTimeFormatter).atStartOfDay(zoneId);
                    long targetDate = targetDateZoneDateTime.toEpochSecond();
                    if (targetDate >= startDate && targetDate <= endDate) {
                        HistoryAndForecastBO historyAndForecastBO = new HistoryAndForecastBO();
                        historyAndForecastBO.setTargetDate(targetDateZoneDateTime.format(dateTimeFormatter2));

                        String occPercentStr = dataElement.element("CF_OCCUPANCY").getTextTrim();
                        if (StringUtils.isNotEmpty(occPercentStr)) {
                            historyAndForecastBO.setOccPercent(new BigDecimal(occPercentStr).setScale(dataPrecision, roundingMode));
                        }

                        String transientRoomsStr = dataElement.element("IND_DEDUCT_ROOMS").getTextTrim();
                        if (StringUtils.isNotEmpty(transientRoomsStr)) {
                            historyAndForecastBO.setTransientRoomNum(Integer.valueOf(transientRoomsStr));
                        }

                        String groupRoomsStr = dataElement.element("GRP_DEDUCT_ROOMS").getTextTrim();
                        if (StringUtils.isNotEmpty(groupRoomsStr)) {
                            historyAndForecastBO.setGroupRoomNum(Integer.valueOf(groupRoomsStr));
                        }

                        String adrStr = dataElement.element("CF_AVERAGE_ROOM_RATE").getTextTrim();
                        if (StringUtils.isNotEmpty(adrStr)) {
                            historyAndForecastBO.setAdr(new BigDecimal(adrStr).setScale(dataPrecision, roundingMode));
                        }

                        historyAndForecastBOS.add(historyAndForecastBO);

                        // 提前结束循环
                        if (targetDate == endDate) {
                            break;
                        }
                    }
                }
            }
            return historyAndForecastBOS;
        }

    }