# OpenBalanceRvController.java

    package cloud.wconnect.frontoffice.controller.guestledgercheck;

    import cloud.wconnect.commonlib.annotation.ApiId;
    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.component.SessionContext;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.exception.ForbiddenException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.I18nMessageUtils;
    import cloud.wconnect.frontoffice.cons.guestledgercheck.GuestLedgerCheckFieldI18nConstant;
    import cloud.wconnect.frontoffice.entity.guestledgercheck.GstLedgOpBalRvNote;
    import cloud.wconnect.frontoffice.entity.guestledgercheck.GstLedgOpBalanceReservation;
    import cloud.wconnect.frontoffice.pojo.dto.guestledgercheck.*;
    import cloud.wconnect.frontoffice.pojo.query.guestledgercheck.OpBalanceRvQuery;
    import cloud.wconnect.frontoffice.pojo.vo.guestledgercheck.OpBalanceRvSummaryVO;
    import cloud.wconnect.frontoffice.service.guestledgercheck.IGstLedgOpBalRvNoteService;
    import cloud.wconnect.frontoffice.service.guestledgercheck.IGstLedgOpBalanceReservationService;
    import cloud.wconnect.frontoffice.service.guestledgercheck.IGstLedgPaidAmountService;
    import cloud.wconnect.frontoffice.service.guestledgercheck.IGstLedgTotalAmountService;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;

    import jakarta.validation.Valid;

    import java.math.BigDecimal;
    import java.util.*;

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

    /**
     * @author lichen
     * @description Reservations with open balance Controller
     * @date 2023/3/17 14:51
     */
    @RestController
    @RequestMapping("/guest-ledger-check/op-balance")
    public class OpenBalanceRvController {

        @Autowired
        private IGstLedgOpBalanceReservationService openBalanceReservationService;
        @Autowired
        private IGstLedgPaidAmountService paidAmountService;
        @Autowired
        private IGstLedgOpBalRvNoteService opBalRvNoteService;
        @Autowired
        private SessionContext sessionContext;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private IGstLedgTotalAmountService totalAmountService;

        /**
         * 获取 Open Balance Reservation 的列表
         *
         * @param property         资产标识
         * @param opBalanceRvQuery 检索条件
         * @return cloud.wconnect.commonlib.pojo.dto.ListPagedDTO<cloud.wconnect.frontoffice.pojo.dto.guestledgercheck.OpBalanceRvDTO>
         * @author lichen
         * @date 2025/8/23
         */
        @ApiId(114)
        @GetMapping
        public ListPagedDTO<OpBalanceRvDTO> page(String property, @Valid OpBalanceRvQuery opBalanceRvQuery) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            return openBalanceReservationService.pageList(propertyDTO, opBalanceRvQuery);
        }

        /**
         * 获取 Open Balance Reservation 的汇总信息
         *
         * @param property 资产标识
         * @param resolved 是否解决 1：是 0：否
         * @return cloud.wconnect.frontoffice.pojo.vo.guestledgercheck.OpBalanceRvSummaryVO
         * @author lichen
         * @date 2025/8/23
         */
        @ApiId(120)
        @GetMapping("/summary")
        public OpBalanceRvSummaryVO summary(String property, @RequestParam Boolean resolved) {
            int propertyId = sessionContext.getPropertyId(property);
            return openBalanceReservationService.summary(propertyId, resolved);
        }

        /**
         * 新增或编辑 Open Balance Reservation 的信息
         *
         * @param rvId     Reservation ID
         * @param rvDto    Reservation 的新增或更新内容
         * @param property 资产标识
         * @return java.util.Map<java.lang.String, java.lang.Long>
         * @author lichen
         * @date 2025/8/23
         */
        @ApiId(115)
        @PostMapping("/{rvId:\\d+}")
        public Map<String, Long> save(@PathVariable long rvId, @Valid @RequestBody OpBalanceRvDTO rvDto, String property) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            int loginUserId = sessionContext.getLoginUserId();

            String crsNumberListStr = StringUtils.join(rvDto.getCrsNumberList(), ",");
            if (crsNumberListStr.length() > 200) {
                throw new BadRequestException(CommonErrorI18nConstant.OVERLENGTH,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("crsNumberListStr", 200),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "The CRS Number List is limited 200 characters");
            }
            //copy 对象
            GstLedgOpBalanceReservation opBalanceRv = beanMapper.map(rvDto, GstLedgOpBalanceReservation.class, or -> {
                or.setCrsNumbers(crsNumberListStr);
            });

            if (rvId == 0) {
                opBalanceRv.setPropertyId(propertyId);
                opBalanceRv.setId(null);
                // 新增 Reservation
                openBalanceReservationService.save(propertyDTO, opBalanceRv, rvDto.getNote(), loginUserId);
                return Collections.singletonMap("id", opBalanceRv.getId());
            }

            checkExistAndBelongProperty(rvId, propertyId);

            opBalanceRv.setId(rvId)
                    .setBalanceAmount(null)//Balance Amount 不做更新
                    .setWriteOffAmount(null)//Write off Amount 不做更新
                    .setLastUpdatedBy(loginUserId)
                    .setLastUpdatedTime(DateTimeUtils.nowTimestamp());
            openBalanceReservationService.updateById(opBalanceRv);
            return Collections.singletonMap("id", rvId);
        }

        /**
         * 删除 Open Reservation
         *
         * @param rvId     Reservation ID
         * @param property 资产标识
         * @return java.util.Map
         * @author lichen
         * @date 2025/8/23
         */
        @ApiId(116)
        @DeleteMapping("/{rvId:\\d+}")
        public Map remove(@PathVariable long rvId, String property) {
            int propertyId = sessionContext.getPropertyId(property);

            GstLedgOpBalanceReservation opBalanceReservation = checkExistAndBelongProperty(rvId, propertyId);

            //删除Reservation及其关联信息
            openBalanceReservationService.remove(opBalanceReservation);
            return Collections.emptyMap();
        }

        /**
         * 获取指定 Reservation 下的 Note 列表
         *
         * @param rvId     Reservation ID
         * @param property 资产标识
         * @return java.util.List<cloud.wconnect.frontoffice.pojo.dto.guestledgercheck.OpBalRvNoteDTO>
         * @author lichen
         * @date 2025/8/23
         */
        @ApiId(117)
        @GetMapping("/{rvId:\\d+}/notes")
        public List<OpBalRvNoteDTO> listNote(@PathVariable long rvId, String property) {
            int propertyId = sessionContext.getPropertyId(property);
            Integer tenantId = sessionContext.getTenantSymbolBO().getId();

            checkExistAndBelongProperty(rvId, propertyId);

            //查询notes
            List<GstLedgOpBalRvNote> list = opBalRvNoteService.listByRvIds(Collections.singletonList(rvId));

            if (CollectionUtils.isEmpty(list)) {
                return Collections.emptyList();
            }
            //查询用户信息
            Set<Integer> userIds = list.stream().map(GstLedgOpBalRvNote::getCreatedBy).collect(toSet());
            ArrayList<Integer> userIdList = new ArrayList<>(userIds.size());
            userIdList.addAll(userIds);
            Map<Integer, String> userId2Name = userInternalService.getNameMapByIds(tenantId, userIdList);
            //List<GstLedgOpBalRvNote> -> List<OpBalRvNoteDTO>
            return beanMapper.mapList(list, OpBalRvNoteDTO.class, (note, noteDTO) -> {
                noteDTO.setCreatedName(userId2Name.get(note.getCreatedBy()));
            });
        }

        /**
         * 新增 Open Reservation 的 Note
         *
         * @param rvId           Reservation Id
         * @param property       资产标识
         * @param opBalRvNoteDTO Note内容
         * @return java.lang.Boolean
         * @author lichen
         * @date 2025/8/23
         */
        @ApiId(118)
        @PostMapping("/{rvId:\\d+}/notes")
        public Boolean saveNote(@PathVariable long rvId, String property, @Valid @RequestBody OpBalRvNoteDTO opBalRvNoteDTO) {
            int propertyId = sessionContext.getPropertyId(property);
            checkExistAndBelongProperty(rvId, propertyId);
            //copy
            GstLedgOpBalRvNote note = beanMapper.map(opBalRvNoteDTO, GstLedgOpBalRvNote.class);
            note.setPropertyId(propertyId);
            note.setCreatedBy(sessionContext.getLoginUserId());
            note.setCreatedTime(DateTimeUtils.nowTimestamp());
            //保存
            return opBalRvNoteService.save(note);
        }

        /**
         * 删除 Open Reservation 下指定的 Note
         *
         * @param rvId     reservation id
         * @param id       note id
         * @param property 资产标识
         * @return emptyMap
         * @author zc
         * @date 2023/6/1
         */
        @ApiId(337)
        @DeleteMapping("/{rvId:\\d+}/notes/{id}")
        public Map<Object, Object> removeNote(@PathVariable long rvId, @PathVariable long id, String property) {
            int propertyId = sessionContext.getPropertyId(property);

            checkExistAndBelongProperty(rvId, propertyId);

            // 校验 note 数据是否存在
            GstLedgOpBalRvNote note = opBalRvNoteService.getById(id);
            if (Objects.isNull(note)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The note is not exist; noteId:" + id);
            }

            // 校验 note 是否归属于对应 Reservation
            if (!note.getOpBalRvId().equals(rvId)) {
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        "The note is not belong to reservation; reservationId:" + rvId + ", noteId:" + id);
            }

            // 只能删除自己创建的 note 数据
            if (!note.getCreatedBy().equals(sessionContext.getLoginUserId())) {
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        "You can only delete data that you created.");
            }

            opBalRvNoteService.removeById(id);

            return Collections.emptyMap();
        }

        /**
         * 编辑 Open Balance Reservation 的 Resolved 状态
         *
         * @param rvId       指定的 Reservation ID
         * @param isResolved 是否解决
         * @param property   资产标识
         * @return Empty Map
         * @author chenx
         * @date 2021/9/8
         */
        @ApiId(119)
        @PutMapping("/{rvId:\\d+}/resolved/{isResolved}")
        public Boolean resolved(@PathVariable long rvId, @PathVariable boolean isResolved, String property) {
            int propertyId = sessionContext.getPropertyId(property);
            checkExistAndBelongProperty(rvId, propertyId);
            //更新指定 Reservation 中的 Resolve 状态
            GstLedgOpBalanceReservation opBalanceReservation = new GstLedgOpBalanceReservation();
            opBalanceReservation.setId(rvId);
            opBalanceReservation.setResolved(isResolved);
            opBalanceReservation.setResolveUpdatedBy(sessionContext.getLoginUserId());
            opBalanceReservation.setResolveUpdatedTime(DateTimeUtils.nowTimestamp());
            return openBalanceReservationService.updateById(opBalanceReservation);
        }

        /**
         * 编辑 Open Balance Reservation 中的 Comment
         *
         * @param rvId       指定的 Reservation ID
         * @param property   资产标识
         * @param commentDTO 评论dto
         * @return java.lang.Boolean
         * @author lichen
         * @date 2025/8/23
         */
        @ApiId(121)
        @PutMapping("/{rvId:\\d+}/comment")
        public Boolean updateComment(@PathVariable long rvId, String property, @RequestBody @Valid CommentDTO commentDTO) {
            int propertyId = sessionContext.getPropertyId(property);
            checkExistAndBelongProperty(rvId, propertyId);
            //更新指定 Reservation 中的Comment
            GstLedgOpBalanceReservation opBalanceReservation = new GstLedgOpBalanceReservation();
            opBalanceReservation.setId(rvId)
                    .setGmComment(commentDTO.getComment())
                    .setGmCommentedBy(sessionContext.getLoginUserId())
                    .setGmCommentedTime(DateTimeUtils.nowTimestamp());
            return openBalanceReservationService.updateById(opBalanceReservation);
        }

        /**
         * 编辑 Open Balance Reservation 中的 Amount Paid
         *
         * @param rvId                 指定的 Reservation ID
         * @param property             资产标识
         * @param amountPaidWrapperDto Amount Paid 删除和新增的内容
         * @return java.util.Map
         * @author lichen
         * @date 2025/8/23
         */
        @ApiId(122)
        @PutMapping("/{rvId:\\d+}/amount-paid")
        public Map updateAmountPaid(@PathVariable long rvId, String property, @Valid @RequestBody AmountPaidWrapperDTO amountPaidWrapperDto) {
            int propertyId = sessionContext.getPropertyId(property);
            int loginUserId = sessionContext.getLoginUserId();

            checkExistAndBelongProperty(rvId, propertyId);
            //存新增的 Amount Paid 记录 并 删除指定的 Amount Paid 记录
            amountPaidWrapperDto.setRvId(rvId);
            amountPaidWrapperDto.setPropertyId(propertyId);
            paidAmountService.saveAndRemove(amountPaidWrapperDto, loginUserId);
            return Collections.emptyMap();
        }

        /**
         * 编辑 Open Balance Reservation 中的各种Amount (Balance Amount, Write off Amount)
         *
         * @param rvId              指定的 Reservation ID
         * @param property          资产标识
         * @param opBalanceRvAmount 更新的Amount类型与值
         * @return java.util.Map
         * @author lichen
         * @date 2025/8/23
         */
        @ApiId(123)
        @PutMapping("/{rvId:\\d+}/amount")
        public Map updateAmount(@PathVariable long rvId, String property, @Valid @RequestBody OpBalanceRvAmountDTO opBalanceRvAmount) {
            int propertyId = sessionContext.getPropertyId(property);
            int loginUserId = sessionContext.getLoginUserId();

            checkExistAndBelongProperty(rvId, propertyId);
            //校验amount 是否符合条件
            BigDecimal newAmount = opBalanceRvAmount.getAmount();
            //根据类型更新对应的Amount
            if (opBalanceRvAmount.getType() == OpBalanceRvAmountDTO.TYPE_BALANCE_AMOUNT) {
                openBalanceReservationService.updateBalanceAmount(rvId, newAmount, loginUserId);
            } else {
                openBalanceReservationService.updateWriteOffAmount(rvId, newAmount, propertyId, loginUserId);
            }
            return Collections.emptyMap();
        }

        /**
         * 重置总损失金额为0
         *
         * @param property 资产标识
         * @return java.util.Map
         * @author lichen
         * @date 2025/8/23
         */
        @ApiId(1613)
        @PostMapping("/reset-zero")
        public Map resetZero(String property) {
            int propertyId = sessionContext.getPropertyId(property);

            //查询未解决的数据
            List<GstLedgOpBalanceReservation> gstLedgOpBalanceReservations = openBalanceReservationService.listByResolved(propertyId, false);
            if(CollectionUtils.isNotEmpty(gstLedgOpBalanceReservations)){
                throw new BadRequestException(CommonErrorI18nConstant.MUST_EMPTY,
                        i18nMessageUtils.getErrorParamMapNeedTranslate(GuestLedgerCheckFieldI18nConstant.I18N_RESERVATIONS_WITH_OUTSTANDING_BALANCE),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "Reservations with outstanding balance must be empty.");
            }

            //重置总损失金额为0
            totalAmountService.resetZero(propertyId);

            return Collections.emptyMap();
        }

        /**
         * 校验是否存在；校验是否归属于当前资产
         *
         * @param rvId       数据id
         * @param propertyId 资产id
         * @return cloud.wconnect.frontoffice.entity.guestledgercheck.GstLedgOpBalanceReservation
         * @author lichen
         * @date 2025/8/23
         */
        private GstLedgOpBalanceReservation checkExistAndBelongProperty(Long rvId, Integer propertyId) {
            GstLedgOpBalanceReservation opBalanceReservation = openBalanceReservationService.getById(rvId);
            if (Objects.isNull(opBalanceReservation)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "id=" + rvId + " ,Reservations with open balance not found");
            }
            if (opBalanceReservation.getPropertyId().compareTo(propertyId) != 0) {
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        "data has no access");
            }
            return opBalanceReservation;
        }

    }