# GmwrMonthEndServiceImpl.java

    package cloud.wconnect.workperformance.service.gmwr.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.RoomsConfigDTO;
    import cloud.wconnect.commonlib.service.RoomsConfigInternalService;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.workperformance.cons.gmwr.monthend.GmwrMonthEndEnum;
    import cloud.wconnect.workperformance.entity.gmwr.*;
    import cloud.wconnect.workperformance.mapper.gmwr.GmwrMonthEndMapper;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.monthend.*;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.monthend.basic.GmwrMonthEndBasicDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.note.GmwrNoteDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.valuedetail.GmwrBasicValueDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.valuedetail.GmwrValueDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.budgetdb.HotelKpiBudgetDatabaseDTO;
    import cloud.wconnect.workperformance.pojo.dto.monthlyreport.internal.MonthlyReportForecastInternalDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.internal.SaleForecastInternalDTO;
    import cloud.wconnect.workperformance.service.gmwr.*;
    import cloud.wconnect.workperformance.service.hotelkpi.internal.IHotelKpiInternalService;
    import cloud.wconnect.workperformance.service.monthlyreport.internal.IMonthlyReportInternalService;
    import cloud.wconnect.workperformance.service.sva.internal.ISvaInternalService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Sets;
    import jakarta.annotation.Resource;
    import org.springframework.stereotype.Service;

    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.LocalDate;
    import java.time.temporal.TemporalAdjusters;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * GM Weekly Report Month End 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-10-12
     */
    @Service
    @DS("work_performance")
    public class GmwrMonthEndServiceImpl extends ServiceImpl<GmwrMonthEndMapper, GmwrMonthEnd> implements IGmwrMonthEndService {
        @Resource
        private IGmwrWeeksService weeksService;
        @Resource
        private IGmwrHistoryAndForecastService historyAndForecastService;
        @Resource
        private IGmwrManagerFlashService managerFlashService;
        @Resource
        private IGmwrMonthEndForecastConfigService monthEndForecastConfigService;
        @Resource
        private IGmwrMonthEndNoteService monthEndNoteService;
        @Resource
        private ISvaInternalService svaInternalService;
        @Resource
        private IMonthlyReportInternalService monthlyReportInternalService;
        @Resource
        private UserInternalService userInternalService;
        @Resource
        private RoomsConfigInternalService roomsConfigInternalService;
        @Resource
        private IHotelKpiInternalService hotelKpiInternalService;
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private ObjectMapper objectMapper;

        /**
         * 根据周报 ID 获取 Month End
         *
         * @param reportId 周报 ID
         * @return GmwrMonthEnd
         * @author zc
         * @date 2024/10/15
         */
        @Override
        public GmwrMonthEnd getByReportId(int reportId) {
            return getOne(new LambdaQueryWrapper<GmwrMonthEnd>()
                    .eq(GmwrMonthEnd::getReportId, reportId));
        }

        /**
         * 获取 Month End
         *
         * @param propertyDTO 资产
         * @param gmwrWeeks   周报数据
         * @return GmwrMonthEndDTO
         * @author zc
         * @date 2024/10/15
         */
        @Override
        public GmwrMonthEndDTO get(PropertyDTO propertyDTO, GmwrWeeks gmwrWeeks) {
            Set<Integer> userIds = Sets.newHashSet();
            GmwrMonthEndDTO monthEndDTO = new GmwrMonthEndDTO();
            GmwrMonthEnd monthEnd = getByReportId(gmwrWeeks.getId());
            if (Objects.nonNull(monthEnd)) {
                List<GmwrMonthEndBasicDetailDTO> monthEndBasicValueList = JsonUtils.convertStringToList(objectMapper,
                        monthEnd.getDetail(), GmwrMonthEndBasicDetailDTO.class);

                RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                        .getInternationalization().getNumber().getCalculationRule());

                List<GmwrMonthEndDetailDTO> list = beanMapper.mapList(monthEndBasicValueList, GmwrMonthEndDetailDTO.class, (item, dto) -> {
                    dto.setCategoryName(GmwrMonthEndEnum.getCategoryName(item.getCategory()));
                    dto.setForecastChg(weeksService.calculateChg(item.getCurrentForecast().getValue(), item.getFirstForecast(), roundingMode));
                    dto.setBudgetChg(weeksService.calculateChg(item.getCurrentForecast().getValue(), item.getBudget().getValue(), roundingMode));
                    dto.setLastYearChg(weeksService.calculateChg(item.getCurrentForecast().getValue(), item.getLastYear(), roundingMode));
                });
                // 获取 Month End Total
                list.add(getMonthEndTotal(list, roundingMode));
                // 汇总 userIds
                userIds.addAll(monthEndBasicValueList.stream().map(item -> item.getCurrentForecast().getUpdatedBy()).toList());
                userIds.addAll(monthEndBasicValueList.stream().map(item -> item.getBudget().getUpdatedBy()).toList());

                monthEndDTO.setFirstForecastUpdatedTime(monthEnd.getFirstForecastUpdatedTime());
                monthEndDTO.setList(list);
            }

            // 获取备注
            List<GmwrMonthEndNote> noteList = monthEndNoteService.listByReportId(gmwrWeeks.getId());
            Map<Integer, GmwrMonthEndNote> category2Note = noteList.stream()
                    .collect(Collectors.toMap(GmwrMonthEndNote::getCategory, Function.identity()));
            userIds.addAll(noteList.stream().map(GmwrMonthEndNote::getUpdatedBy).toList());

            // 获取用户名
            Map<Integer, String> userId2Name = userInternalService.getNameMapByIds(propertyDTO.getTenantId(), userIds);
            monthEndDTO.getList().forEach(item -> {
                // 设置更新人
                item.getCurrentForecast().setUpdatedName(userId2Name.get(item.getCurrentForecast().getUpdatedBy()));
                item.getBudget().setUpdatedName(userId2Name.get(item.getBudget().getUpdatedBy()));

                // 设置备注
                GmwrMonthEndNote note = category2Note.get(item.getCategory());
                if (Objects.nonNull(note)) {
                    GmwrNoteDTO noteDTO = beanMapper.map(note, GmwrNoteDTO.class, dto ->
                            dto.setUpdatedName(userId2Name.get(dto.getUpdatedBy())));
                    item.setNote(noteDTO);
                }
            });
            return monthEndDTO;
        }

        /**
         * 获取 Month End Total
         *
         * @param list         数值集合
         * @param roundingMode 舍入模式
         * @return GmwrMonthEndValueDTO
         * @author zc
         * @date 2025/2/28
         */
        private GmwrMonthEndDetailDTO getMonthEndTotal(List<GmwrMonthEndDetailDTO> list, RoundingMode roundingMode) {
            GmwrMonthEndDetailDTO totalDTO = new GmwrMonthEndDetailDTO();
            totalDTO.setCategory(GmwrMonthEndEnum.TOTAL_REVENUE.getCategory());
            totalDTO.setCategoryName(GmwrMonthEndEnum.TOTAL_REVENUE.getCategoryName());

            List<Integer> revenueCategoryList = List.of(GmwrMonthEndEnum.ROOM_REVENUE.getCategory(),
                    GmwrMonthEndEnum.FB_REVENUE.getCategory(), GmwrMonthEndEnum.OTHERS_REVENUE.getCategory());
            BigDecimal totalCurrentForecast = list.stream().filter(item ->
                            revenueCategoryList.contains(item.getCategory())).map(item -> item.getCurrentForecast().getValue())
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
            totalDTO.setCurrentForecast(new GmwrValueDetailDTO().setValue(totalCurrentForecast));
            BigDecimal totalFirstForecast = list.stream().filter(item ->
                            revenueCategoryList.contains(item.getCategory())).map(GmwrMonthEndDetailDTO::getFirstForecast)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
            totalDTO.setFirstForecast(totalFirstForecast);
            BigDecimal totalForecastChg = weeksService.calculateChg(totalCurrentForecast, totalFirstForecast, roundingMode);
            totalDTO.setForecastChg(totalForecastChg);
            BigDecimal totalBudget = list.stream().filter(item ->
                            revenueCategoryList.contains(item.getCategory())).map(item -> item.getBudget().getValue())
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
            totalDTO.setBudget(new GmwrValueDetailDTO().setValue(totalBudget));
            BigDecimal totalBudgetChg = weeksService.calculateChg(totalCurrentForecast, totalBudget, roundingMode);
            totalDTO.setBudgetChg(totalBudgetChg);
            BigDecimal totalLastYear = list.stream().filter(item ->
                            revenueCategoryList.contains(item.getCategory())).map(GmwrMonthEndDetailDTO::getLastYear)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
            totalDTO.setLastYear(totalLastYear);
            BigDecimal totalLastYearChg = weeksService.calculateChg(totalCurrentForecast, totalLastYear, roundingMode);
            totalDTO.setLastYearChg(totalLastYearChg);
            return totalDTO;
        }

        /**
         * 保存 Month End Budget
         *
         * @param propertyId    资产 id
         * @param reportId      报告 id
         * @param budgetDTOList 数据集合
         * @param operatorId    操作人 id
         * @author zc
         * @date 2025/3/1
         */
        @Override
        public void saveBudget(int propertyId, int reportId, List<GmwrMonthEndSaveBudgetDTO> budgetDTOList, int operatorId) {
            Map<Integer, GmwrMonthEndSaveBudgetDTO> category2BudgetDTO = budgetDTOList.stream()
                    .collect(Collectors.toMap(GmwrMonthEndSaveBudgetDTO::getCategory, Function.identity()));

            GmwrMonthEnd monthEnd = getByReportId(reportId);
            List<GmwrMonthEndBasicDetailDTO> monthEndBasicValueDTOList;
            if (Objects.nonNull(monthEnd)) {
                monthEndBasicValueDTOList = JsonUtils.convertStringToList(objectMapper, monthEnd.getDetail(), GmwrMonthEndBasicDetailDTO.class);
            } else {
                monthEnd = new GmwrMonthEnd();
                monthEnd.setPropertyId(propertyId);
                monthEnd.setReportId(reportId);
                monthEnd.setFirstForecastUpdatedTime(0L);

                // 生成 BasicValueDTO 初始值
                monthEndBasicValueDTOList = generateInitBasicValueDTOList();
            }
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            monthEndBasicValueDTOList.forEach(item -> {
                GmwrMonthEndSaveBudgetDTO saveBudgetDTO = category2BudgetDTO.get(item.getCategory());
                if (Objects.isNull(saveBudgetDTO)) {
                    return;
                }
                weeksService.editValue(item.getBudget(), saveBudgetDTO.getValue(), operatorId, nowTimestamp);
            });
            monthEnd.setDetail(JsonUtils.convertObjListToString(objectMapper, monthEndBasicValueDTOList));
            baseMapper.insertOrUpdateByUnique(monthEnd);
        }

        /**
         * 保存 Month End Current Forecast Others
         *
         * @param propertyId 资产 id
         * @param reportId   周报 id
         * @param othersDTO  Others 数据
         * @param operatorId 操作人 id
         * @author zc
         * @date 2025/4/25
         */
        @Override
        public void saveCurrentForecastOthers(int propertyId, int reportId, GmwrMonthEndSaveOthersDTO othersDTO, int operatorId) {
            GmwrMonthEnd monthEnd = getByReportId(reportId);
            List<GmwrMonthEndBasicDetailDTO> monthEndBasicValueDTOList;
            if (Objects.nonNull(monthEnd)) {
                monthEndBasicValueDTOList = JsonUtils.convertStringToList(objectMapper, monthEnd.getDetail(), GmwrMonthEndBasicDetailDTO.class);
            } else {
                monthEnd = new GmwrMonthEnd();
                monthEnd.setPropertyId(propertyId);
                monthEnd.setReportId(reportId);
                monthEnd.setFirstForecastUpdatedTime(0L);

                // 生成 BasicValueDTO 初始值
                monthEndBasicValueDTOList = generateInitBasicValueDTOList();
            }
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            monthEndBasicValueDTOList.forEach(item -> {
                if (item.getCategory() == GmwrMonthEndEnum.OTHERS_REVENUE.getCategory()) {
                    weeksService.editValue(item.getCurrentForecast(), othersDTO.getValue(), operatorId, nowTimestamp);
                }
            });
            monthEnd.setDetail(JsonUtils.convertObjListToString(objectMapper, monthEndBasicValueDTOList));
            baseMapper.insertOrUpdateByUnique(monthEnd);
        }

        /**
         * 生成 Month End
         *
         * @param propertyDTO 资产
         * @param gmwrWeeks   周报数据
         * @author zc
         * @date 2024/10/16
         */
        @Override
        public void generate(PropertyDTO propertyDTO, GmwrWeeks gmwrWeeks) {
            LocalDate weekStartDate = DateTimeUtils.strToLocalDate(gmwrWeeks.getWeek().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);

            GmwrMonthEnd monthEnd = getByReportId(gmwrWeeks.getId());
            List<GmwrMonthEndBasicDetailDTO> monthEndBasicValueDTOList;
            if (Objects.nonNull(monthEnd)) {
                monthEndBasicValueDTOList = JsonUtils.convertStringToList(objectMapper, monthEnd.getDetail(), GmwrMonthEndBasicDetailDTO.class);
            } else {
                monthEnd = new GmwrMonthEnd();
                monthEnd.setPropertyId(propertyDTO.getId());
                monthEnd.setReportId(gmwrWeeks.getId());

                // 生成 BasicValueDTO 初始值
                monthEndBasicValueDTOList = generateInitBasicValueDTOList();
            }
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            // 获取资产房间数配置
            RoomsConfigDTO roomsConfigDTO = roomsConfigInternalService.getRoomsConfig(propertyDTO.getId());
            Integer rooms = roomsConfigDTO.getRooms();

            // 获取月初到月末的 Current Forecast
            long startDate = Long.parseLong(DateTimeUtils.localDateToStr(weekStartDate.withDayOfMonth(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDate = Long.parseLong(DateTimeUtils.localDateToStr(weekStartDate.with(TemporalAdjusters.lastDayOfMonth()),
                    BaseConstant.DATE_FORMAT_YYYYMMDD));
            List<SaleForecastInternalDTO> saleForecastInternalDTOList = svaInternalService.listSaleForecast(propertyDTO, startDate, endDate);
            // 组装 Current Forecast
            packageCurrentForecast(rooms, saleForecastInternalDTOList, roundingMode, monthEndBasicValueDTOList);
            List<GmwrMonthEndForecastConfigDTO> forecastConfigDTOList = monthEndForecastConfigService.get(propertyDTO.getId());

            // 获取 First Forecast
            long yearMonth = Long.parseLong(DateTimeUtils.localDateToStr(weekStartDate, BaseConstant.DATE_FORMAT_YYYYMM));
            List<MonthlyReportForecastInternalDTO> forecastInternalDTOList = monthlyReportInternalService.listFirstForecast(propertyDTO, yearMonth);
            // 组装 First Forecast
            packageFirstForecast(forecastConfigDTOList, forecastInternalDTOList, monthEndBasicValueDTOList);

            // 获取去年同月的月初到月末的 History and Forecast 数据
            long lastYearStartDate = Long.parseLong(DateTimeUtils.localDateToStr(weekStartDate.minusYears(1)
                    .withDayOfMonth(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
            long lastYearEndDate = Long.parseLong(DateTimeUtils.localDateToStr(weekStartDate.minusYears(1)
                    .with(TemporalAdjusters.lastDayOfMonth()), BaseConstant.DATE_FORMAT_YYYYMMDD));
            List<GmwrHistoryAndForecast> historyAndForecastList = historyAndForecastService.list(propertyDTO, lastYearStartDate, lastYearEndDate);
            // 获取去年同月的 Manager Flash 数据
            long lastYearMonth = Long.parseLong(DateTimeUtils.localDateToStr(weekStartDate.minusYears(1), BaseConstant.DATE_FORMAT_YYYYMM));
            GmwrManagerFlash managerFlash = managerFlashService.get(propertyDTO, lastYearMonth);
            // 组装 Last Year
            packageLastYear(rooms, historyAndForecastList, managerFlash, roundingMode, monthEndBasicValueDTOList);

            // 获取 Budget Database 数据
            HotelKpiBudgetDatabaseDTO budgetDTO = hotelKpiInternalService.getBudgetByYear(propertyDTO.getId(), weekStartDate.getYear());
            // 组装 Budget
            packageBudget(rooms, weekStartDate.lengthOfMonth(), yearMonth, budgetDTO, roundingMode, monthEndBasicValueDTOList);

            monthEnd.setDetail(JsonUtils.convertObjListToString(objectMapper, monthEndBasicValueDTOList));

            // 设置 FirstForecastUpdatedTime
            MonthlyReportForecastInternalDTO maxUpdatedTimeData = forecastInternalDTOList.stream()
                    .max(Comparator.comparing(MonthlyReportForecastInternalDTO::getUpdateTime)).orElse(new MonthlyReportForecastInternalDTO());
            long firstForecastUpdatedTime = Objects.isNull(maxUpdatedTimeData.getUpdateTime()) ? 0L :
                    Long.parseLong(DateTimeUtils.timestampToStr(maxUpdatedTimeData.getUpdateTime(),
                            BaseConstant.DATE_FORMAT_YYYYMMDD, propertyDTO.getTimezone()));
            monthEnd.setFirstForecastUpdatedTime(firstForecastUpdatedTime);
            baseMapper.insertOrUpdateByUnique(monthEnd);
        }

        /**
         * 组装 Budget
         *
         * @param rooms        房间数
         * @param days         天数
         * @param budgetDTO    Budget 数据
         * @param roundingMode 舍入模式
         * @param valueDTOList 数据DTO集合
         * @author zc
         * @date 2025/8/28
         */
        private void packageBudget(Integer rooms, int days, long yearMonth, HotelKpiBudgetDatabaseDTO budgetDTO,
                                   RoundingMode roundingMode, List<GmwrMonthEndBasicDetailDTO> valueDTOList) {
            valueDTOList.forEach(item -> {
                int category = item.getCategory();
                if (category == GmwrMonthEndEnum.OCC_PERCENT.getCategory()) {
                    Long roomsSold = hotelKpiInternalService.getRoomsSoldBudget(budgetDTO, yearMonth, yearMonth);
                    BigDecimal occPercent = calculateOccPercent(rooms, roomsSold, days, roundingMode);
                    item.getBudget().setValue(occPercent);
                } else if (category == GmwrMonthEndEnum.REV_PAR.getCategory()) {
                    BigDecimal roomRevenue = hotelKpiInternalService.getRoomRevenueBudget(budgetDTO, yearMonth, yearMonth);
                    BigDecimal revPar = calculateRevPar(rooms, roomRevenue, days, roundingMode);
                    item.getBudget().setValue(revPar);
                } else if (category == GmwrMonthEndEnum.ADR.getCategory()) {
                    Long roomsSold = hotelKpiInternalService.getRoomsSoldBudget(budgetDTO, yearMonth, yearMonth);
                    BigDecimal roomRevenue = hotelKpiInternalService.getRoomRevenueBudget(budgetDTO, yearMonth, yearMonth);
                    BigDecimal adr = calculateAdr(roomRevenue, roomsSold, roundingMode);
                    item.getBudget().setValue(adr);
                } else if (category == GmwrMonthEndEnum.ROOM_REVENUE.getCategory()) {
                    BigDecimal roomRevenue = hotelKpiInternalService.getRoomRevenueBudget(budgetDTO, yearMonth, yearMonth);
                    item.getBudget().setValue(roomRevenue);
                } else if (category == GmwrMonthEndEnum.FB_REVENUE.getCategory()) {
                    BigDecimal fbRevenue = hotelKpiInternalService.getFBRevenueBudget(budgetDTO, yearMonth, yearMonth);
                    item.getBudget().setValue(fbRevenue);
                } else if (category == GmwrMonthEndEnum.OTHERS_REVENUE.getCategory()) {
                    BigDecimal otherRevenue = hotelKpiInternalService.getOtherRevenueBudget(budgetDTO, yearMonth, yearMonth);
                    item.getBudget().setValue(otherRevenue);
                }
            });
        }

        /**
         * 组装 Current Forecast
         *
         * @param rooms            房间数
         * @param saleForecastList Sale Forecast 数据
         * @param roundingMode     舍入模式
         * @param valueDTOList     数据DTO集合
         * @author zc
         * @date 2024/10/16
         */
        private void packageCurrentForecast(Integer rooms, List<SaleForecastInternalDTO> saleForecastList,
                                            RoundingMode roundingMode, List<GmwrMonthEndBasicDetailDTO> valueDTOList) {
            List<Integer> roomsSoldList = saleForecastList.stream().map(SaleForecastInternalDTO::getEstiRoomSales)
                    .filter(Objects::nonNull).toList();
            int effectiveDaysForRoomsSold = roomsSoldList.size();
            Long roomsSoldTotal = roomsSoldList.stream().reduce(Integer::sum).map(Integer::longValue).orElse(null);

            // 房间售出数量 * 平均房价 的集合
            List<BigDecimal> roomRevenueList = saleForecastList.stream().filter(item -> Objects.nonNull(item.getEstiAdr())
                    && Objects.nonNull(item.getEstiRoomSales())).map(item ->
                    new BigDecimal(String.valueOf(item.getEstiRoomSales())).multiply(item.getEstiAdr())).toList();
            int effectiveDaysForRoomRevenue = roomRevenueList.size();
            BigDecimal roomRevenueTotal = roomRevenueList.stream().reduce(BigDecimal::add).orElse(null);

            valueDTOList.forEach(item -> {
                int category = item.getCategory();
                BigDecimal currentForecast = null;
                if (category == GmwrMonthEndEnum.OCC_PERCENT.getCategory()) {
                    currentForecast = calculateOccPercent(rooms, roomsSoldTotal, effectiveDaysForRoomsSold, roundingMode);
                } else if (category == GmwrMonthEndEnum.REV_PAR.getCategory()) {
                    currentForecast = calculateRevPar(rooms, roomRevenueTotal, effectiveDaysForRoomRevenue, roundingMode);
                } else if (category == GmwrMonthEndEnum.ADR.getCategory()) {
                    currentForecast = calculateAdr(roomRevenueTotal, roomsSoldTotal, roundingMode);
                } else if (category == GmwrMonthEndEnum.ROOM_REVENUE.getCategory()) {
                    if (Objects.nonNull(roomRevenueTotal)) {
                        currentForecast = roomRevenueTotal.setScale(BaseConstant.DATA_PRECISION, roundingMode);
                    }
                } else if (category == GmwrMonthEndEnum.FB_REVENUE.getCategory()) {
                    currentForecast = saleForecastList.stream().map(SaleForecastInternalDTO::getEstiFbRev)
                            .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                }
                item.setCurrentForecast(new GmwrBasicValueDetailDTO().setValue(currentForecast));
            });
        }

        /**
         * 组装 First Forecast
         *
         * @param configDTOList 配置集合
         * @param forecastList  Forecast 数据
         * @param valueDTOList  数据DTO集合
         * @author zc
         * @date 2024/10/16
         */
        private void packageFirstForecast(List<GmwrMonthEndForecastConfigDTO> configDTOList, List<MonthlyReportForecastInternalDTO> forecastList,
                                          List<GmwrMonthEndBasicDetailDTO> valueDTOList) {
            Map<String, MonthlyReportForecastInternalDTO> categoryName2ForecastDTO = forecastList.stream()
                    .collect(Collectors.toMap(MonthlyReportForecastInternalDTO::getCategoryName, Function.identity()));

            Map<Integer, GmwrMonthEndForecastConfigDTO> category2ConfigDTO = configDTOList.stream()
                    .collect(Collectors.toMap(GmwrMonthEndForecastConfigDTO::getCategory, Function.identity()));

            valueDTOList.forEach(item -> {
                GmwrMonthEndForecastConfigDTO configDTO = category2ConfigDTO.getOrDefault(item.getCategory(), new GmwrMonthEndForecastConfigDTO());
                MonthlyReportForecastInternalDTO internalDTO = categoryName2ForecastDTO.getOrDefault(configDTO.getMappingName(),
                        new MonthlyReportForecastInternalDTO());
                item.setFirstForecast(internalDTO.getForecast());
            });
        }

        /**
         * 组装 Last Year
         *
         * @param rooms        房间数
         * @param hfList       History and Forecast 数据
         * @param managerFlash Manager Flash 数据
         * @param roundingMode 舍入模式
         * @param valueDTOList 数据DTO集合
         * @author zc
         * @date 2024/10/16
         */
        private void packageLastYear(Integer rooms, List<GmwrHistoryAndForecast> hfList, GmwrManagerFlash managerFlash,
                                     RoundingMode roundingMode, List<GmwrMonthEndBasicDetailDTO> valueDTOList) {
            List<Integer> roomsSoldList = hfList.stream().map(GmwrHistoryAndForecast::getRoomsSold)
                    .filter(Objects::nonNull).toList();
            int effectiveDaysForRoomsSold = roomsSoldList.size();
            Long roomsSoldTotal = roomsSoldList.stream().reduce(Integer::sum).map(Integer::longValue).orElse(null);

            // 房间售出数量 * 平均房价 的集合
            List<BigDecimal> roomRevenueList = hfList.stream().map(GmwrHistoryAndForecast::getRoomRevenue)
                    .filter(Objects::nonNull).toList();
            int effectiveDaysForRoomRevenue = roomRevenueList.size();
            BigDecimal roomRevenueTotal = roomRevenueList.stream().reduce(BigDecimal::add).orElse(null);

            valueDTOList.forEach(item -> {
                int category = item.getCategory();
                if (category == GmwrMonthEndEnum.OCC_PERCENT.getCategory()) {
                    item.setLastYear(calculateOccPercent(rooms, roomsSoldTotal, effectiveDaysForRoomsSold, roundingMode));
                } else if (category == GmwrMonthEndEnum.REV_PAR.getCategory()) {
                    item.setLastYear(calculateRevPar(rooms, roomRevenueTotal, effectiveDaysForRoomRevenue, roundingMode));
                } else if (category == GmwrMonthEndEnum.ADR.getCategory()) {
                    item.setLastYear(calculateAdr(roomRevenueTotal, roomsSoldTotal, roundingMode));
                } else if (category == GmwrMonthEndEnum.ROOM_REVENUE.getCategory()) {
                    item.setLastYear(roomRevenueTotal);
                } else if (category == GmwrMonthEndEnum.FB_REVENUE.getCategory()) {
                    if (Objects.nonNull(managerFlash)) {
                        item.setLastYear(managerFlash.getFbRevenue());
                    }
                } else if (category == GmwrMonthEndEnum.OTHERS_REVENUE.getCategory()) {
                    if (Objects.nonNull(managerFlash)) {
                        item.setLastYear(managerFlash.getOthersRevenue());
                    }
                }
            });
        }

        /**
         * 计算 OCC %
         *
         * @param rooms        房间数
         * @param roomsSold    售出房间数
         * @param days         天数
         * @param roundingMode 舍入模式
         * @return java.math.BigDecimal
         * @author zc
         * @date 2024/10/16
         */
        private BigDecimal calculateOccPercent(Integer rooms, Long roomsSold, int days, RoundingMode roundingMode) {
            if (days == 0) {
                return null;
            }
            if (Objects.isNull(rooms) || rooms == 0) {
                return null;
            }
            if (Objects.isNull(roomsSold)) {
                return null;
            }
            return new BigDecimal(String.valueOf(roomsSold)).multiply(new BigDecimal("100"))
                    .divide(new BigDecimal(String.valueOf(rooms)).multiply(new BigDecimal(String.valueOf(days))),
                            BaseConstant.DATA_PRECISION, roundingMode);
        }

        /**
         * 计算 Rev Par
         *
         * @param rooms        房间数
         * @param roomRevenue  房间收入
         * @param days         天数
         * @param roundingMode 舍入模式
         * @return java.math.BigDecimal
         * @author zc
         * @date 2024/10/16
         */
        private BigDecimal calculateRevPar(Integer rooms, BigDecimal roomRevenue, int days, RoundingMode roundingMode) {
            if (days == 0) {
                return null;
            }
            if (Objects.isNull(rooms) || rooms == 0) {
                return null;
            }
            if (Objects.isNull(roomRevenue)) {
                return null;
            }
            return roomRevenue.divide(new BigDecimal(String.valueOf(rooms))
                    .multiply(new BigDecimal(String.valueOf(days))), BaseConstant.DATA_PRECISION, roundingMode);
        }

        /**
         * 计算 ADR
         *
         * @param roomRevenue  房间收入
         * @param roomsSold    售出房间数
         * @param roundingMode 舍入模式
         * @return java.math.BigDecimal
         * @author zc
         * @date 2024/10/16
         */
        private BigDecimal calculateAdr(BigDecimal roomRevenue, Long roomsSold, RoundingMode roundingMode) {
            if (Objects.isNull(roomsSold) || roomsSold == 0) {
                return null;
            }
            if (Objects.isNull(roomRevenue)) {
                return null;
            }
            return roomRevenue.divide(new BigDecimal(String.valueOf(roomsSold)), BaseConstant.DATA_PRECISION, roundingMode);
        }

        /**
         * 生成 BasicValueDTO 初始值
         *
         * @return List<GmwrMonthEndBasicValueDTO>
         * @author zc
         * @date 2025/3/1
         */
        private List<GmwrMonthEndBasicDetailDTO> generateInitBasicValueDTOList() {
            return Lists.newArrayList(GmwrMonthEndEnum.OCC_PERCENT, GmwrMonthEndEnum.REV_PAR,
                    GmwrMonthEndEnum.ADR, GmwrMonthEndEnum.ROOM_REVENUE, GmwrMonthEndEnum.FB_REVENUE,
                    GmwrMonthEndEnum.OTHERS_REVENUE).stream().map(item -> {
                GmwrMonthEndBasicDetailDTO basicValueDTO = new GmwrMonthEndBasicDetailDTO();
                basicValueDTO.setCategory(item.getCategory());
                return basicValueDTO;
            }).toList();
        }

    }