@Override
  @PlusTransactional
  public ResponseEvent<List<DpRequirementDetail>> getRequirements(RequestEvent<Long> req) {
    try {
      AccessCtrlMgr.getInstance().ensureUserIsAdmin();
      Long dpId = req.getPayload();
      DistributionProtocol dp = daoFactory.getDistributionProtocolDao().getById(dpId);
      if (dp == null) {
        return ResponseEvent.userError(DistributionProtocolErrorCode.NOT_FOUND);
      }

      List<DpRequirementDetail> reqDetails = DpRequirementDetail.from(dp.getRequirements());
      Map<Long, BigDecimal> distributedQty = getDprDao().getDistributedQtyByDp(dpId);
      for (DpRequirementDetail reqDetail : reqDetails) {
        BigDecimal qty = distributedQty.get(reqDetail.getId());
        reqDetail.setDistributedQty(qty == null ? BigDecimal.ZERO : qty);
      }

      return ResponseEvent.response(reqDetails);
    } catch (OpenSpecimenException ose) {
      return ResponseEvent.error(ose);
    } catch (Exception e) {
      return ResponseEvent.serverError(e);
    }
  }
  @Override
  @PlusTransactional
  public ResponseEvent<DistributionProtocolDetail> updateDistributionProtocol(
      RequestEvent<DistributionProtocolDetail> req) {
    try {
      AccessCtrlMgr.getInstance().ensureUserIsAdmin();

      Long protocolId = req.getPayload().getId();
      String title = req.getPayload().getTitle();

      DistributionProtocol existing = null;
      if (protocolId != null) {
        existing = daoFactory.getDistributionProtocolDao().getById(protocolId);
      } else {
        existing = daoFactory.getDistributionProtocolDao().getDistributionProtocol(title);
      }

      if (existing == null) {
        return ResponseEvent.userError(DistributionProtocolErrorCode.NOT_FOUND);
      }

      DistributionProtocol distributionProtocol =
          distributionProtocolFactory.createDistributionProtocol(req.getPayload());
      ensureUniqueConstraints(distributionProtocol, existing);

      existing.update(distributionProtocol);
      daoFactory.getDistributionProtocolDao().saveOrUpdate(existing);
      return ResponseEvent.response(DistributionProtocolDetail.from(existing));
    } catch (OpenSpecimenException ose) {
      return ResponseEvent.error(ose);
    } catch (Exception ex) {
      return ResponseEvent.serverError(ex);
    }
  }
  private boolean isUniqueShortTitle(DistributionProtocol newDp, DistributionProtocol existingDp) {
    if (existingDp != null && newDp.getShortTitle().equals(existingDp.getShortTitle())) {
      return true;
    }

    DistributionProtocol existing =
        daoFactory.getDistributionProtocolDao().getByShortTitle(newDp.getShortTitle());
    if (existing != null) {
      return false;
    }

    return true;
  }
  private void ensureUniqueConstraints(
      DistributionProtocol newDp, DistributionProtocol existingDp) {
    OpenSpecimenException ose = new OpenSpecimenException(ErrorType.USER_ERROR);

    if (!isUniqueTitle(newDp, existingDp)) {
      ose.addError(DistributionProtocolErrorCode.DUP_TITLE, newDp.getTitle());
    }

    if (!isUniqueShortTitle(newDp, existingDp)) {
      ose.addError(DistributionProtocolErrorCode.DUP_SHORT_TITLE, newDp.getShortTitle());
    }

    ose.checkAndThrow();
  }
  @Override
  @PlusTransactional
  public ResponseEvent<List<DependentEntityDetail>> getDependentEntities(RequestEvent<Long> req) {
    try {
      DistributionProtocol existing =
          daoFactory.getDistributionProtocolDao().getById(req.getPayload());
      if (existing == null) {
        return ResponseEvent.userError(DistributionProtocolErrorCode.NOT_FOUND);
      }

      return ResponseEvent.response(existing.getDependentEntities());
    } catch (Exception e) {
      return ResponseEvent.serverError(e);
    }
  }
  private void ensureUniqueReqConstraints(
      DpRequirement oldDpr, DpRequirement newDpr, OpenSpecimenException ose) {
    if (oldDpr != null && oldDpr.equalsSpecimenGroup(newDpr)) {
      return;
    }

    DistributionProtocol dp = newDpr.getDistributionProtocol();
    if (dp.hasRequirement(
        newDpr.getSpecimenType(), newDpr.getAnatomicSite(), newDpr.getPathologyStatus())) {
      ose.addError(
          DpRequirementErrorCode.ALREADY_EXISTS,
          newDpr.getSpecimenType(),
          newDpr.getAnatomicSite(),
          newDpr.getPathologyStatus());
    }
  }
  @Override
  @PlusTransactional
  public ResponseEvent<DistributionProtocolDetail> deleteDistributionProtocol(
      RequestEvent<Long> req) {
    try {
      AccessCtrlMgr.getInstance().ensureUserIsAdmin();

      DistributionProtocol existing =
          daoFactory.getDistributionProtocolDao().getById(req.getPayload());
      if (existing == null) {
        return ResponseEvent.userError(DistributionProtocolErrorCode.NOT_FOUND);
      }

      existing.delete();
      daoFactory.getDistributionProtocolDao().saveOrUpdate(existing);
      return ResponseEvent.response(DistributionProtocolDetail.from(existing));
    } catch (OpenSpecimenException ose) {
      return ResponseEvent.error(ose);
    } catch (Exception e) {
      return ResponseEvent.serverError(e);
    }
  }
  @Override
  @PlusTransactional
  public ResponseEvent<DistributionProtocolDetail> updateActivityStatus(
      RequestEvent<DistributionProtocolDetail> req) {
    try {
      AccessCtrlMgr.getInstance().ensureUserIsAdmin();

      Long dpId = req.getPayload().getId();
      String status = req.getPayload().getActivityStatus();
      if (StringUtils.isBlank(status) || !Status.isValidActivityStatus(status)) {
        return ResponseEvent.userError(ActivityStatusErrorCode.INVALID);
      }

      DistributionProtocol existing = daoFactory.getDistributionProtocolDao().getById(dpId);
      if (existing == null) {
        return ResponseEvent.userError(DistributionProtocolErrorCode.NOT_FOUND);
      }

      if (existing.getActivityStatus().equals(status)) {
        return ResponseEvent.response(DistributionProtocolDetail.from(existing));
      }

      if (status.equals(Status.ACTIVITY_STATUS_DISABLED.getStatus())) {
        existing.delete();
      } else {
        existing.setActivityStatus(status);
      }

      daoFactory.getDistributionProtocolDao().saveOrUpdate(existing);
      return ResponseEvent.response(DistributionProtocolDetail.from(existing));
    } catch (OpenSpecimenException ose) {
      return ResponseEvent.error(ose);
    } catch (Exception e) {
      return ResponseEvent.serverError(e);
    }
  }