public Integer createUpdate(Integer executorId, BillingProcessConfigurationDTO dto) {
    configuration = configurationDas.findByEntity(dto.getEntity());
    if (configuration != null) {

      if (!configuration.getGenerateReport().equals(dto.getGenerateReport())) {
        eLogger.audit(
            executorId,
            null,
            Constants.TABLE_BILLING_PROCESS_CONFIGURATION,
            configuration.getId(),
            EventLogger.MODULE_BILLING_PROCESS,
            EventLogger.ROW_UPDATED,
            new Integer(configuration.getGenerateReport()),
            null,
            null);
        configuration.setGenerateReport(dto.getGenerateReport());
        configuration.setReviewStatus(
            dto.getGenerateReport() == 1
                ? Constants.REVIEW_STATUS_GENERATED
                : Constants.REVIEW_STATUS_APPROVED);
      } else {
        eLogger.audit(
            executorId,
            null,
            Constants.TABLE_BILLING_PROCESS_CONFIGURATION,
            configuration.getId(),
            EventLogger.MODULE_BILLING_PROCESS,
            EventLogger.ROW_UPDATED,
            null,
            null,
            null);
      }

      configuration.setNextRunDate(dto.getNextRunDate());
    } else {
      configuration =
          configurationDas.create(dto.getEntity(), dto.getNextRunDate(), dto.getGenerateReport());
    }

    configuration.setDaysForReport(dto.getDaysForReport());
    configuration.setDaysForRetry(dto.getDaysForRetry());
    configuration.setRetries(dto.getRetries());
    configuration.setPeriodUnit(dto.getPeriodUnit());
    configuration.setPeriodValue(dto.getPeriodValue());
    configuration.setDueDateUnitId(dto.getDueDateUnitId());
    configuration.setDueDateValue(dto.getDueDateValue());
    configuration.setDfFm(dto.getDfFm());
    configuration.setOnlyRecurring(dto.getOnlyRecurring());
    configuration.setInvoiceDateProcess(dto.getInvoiceDateProcess());
    configuration.setAutoPayment(dto.getAutoPayment());
    configuration.setAutoPaymentApplication(dto.getAutoPaymentApplication());
    configuration.setMaximumPeriods(dto.getMaximumPeriods());

    return configuration.getId();
  }
  private void init() {
    eLogger = EventLogger.getInstance();
    billingProcessDas = new BillingProcessDAS();

    // now create the run info row
    processRunHome = new ProcessRunDAS();
  }
  /**
   * Moves a user one step forward in the ageing process (move from active -> suspended etc.). The
   * user will only be moved if they have spent long enough in their present status.
   *
   * @param steps ageing steps
   * @param user user to age
   * @param today today's date
   * @return the resulting ageing step for the user after ageing
   */
  public AgeingEntityStepDTO ageUser(
      Set<AgeingEntityStepDTO> steps, UserDTO user, Date today, Integer executorId) {
    LOG.debug("Ageing user " + user.getId());

    Integer currentStatusId = user.getStatus().getId();
    UserStatusDTO nextStatus = null;
    AgeingEntityStepDTO ageingStep = null;

    if (currentStatusId.equals(UserDTOEx.STATUS_ACTIVE)) {
      // send welcome message (initial step after active).
      nextStatus = getNextAgeingStep(steps, UserDTOEx.STATUS_ACTIVE);

    } else {
      // user already in the ageing process
      ageingStep = new AgeingEntityStepDAS().findStep(user.getEntity().getId(), currentStatusId);

      if (ageingStep != null) {
        // determine the next ageing step
        if (isAgeingRequired(user, ageingStep, today)) {
          nextStatus = getNextAgeingStep(steps, currentStatusId);
          LOG.debug(
              "User "
                  + user.getId()
                  + " needs to be aged to '"
                  + getStatusDescription(nextStatus)
                  + "'");
        }

      } else {
        // User is in a non-existent ageing status... Either the status was removed or
        // the data is bad. As a workaround, just move to the next status.
        nextStatus = getNextAgeingStep(steps, currentStatusId);
        LOG.warn(
            "User "
                + user.getId()
                + " is in an invalid ageing step. Moving to '"
                + getStatusDescription(nextStatus)
                + "'");
      }
    }

    // set status
    if (nextStatus != null) {
      setUserStatus(user, nextStatus, today, null);

    } else {
      LOG.debug("Next status is null, no further ageing steps are available.");
      eLogger.warning(
          user.getEntity().getId(),
          user.getUserId(),
          user.getUserId(),
          EventLogger.MODULE_USER_MAINTENANCE,
          EventLogger.NO_FURTHER_STEP,
          Constants.TABLE_BASE_USER);
    }

    return ageingStep;
  }
  public void setReviewApproval(Integer executorId, boolean flag) {

    eLogger.audit(
        executorId,
        null,
        Constants.TABLE_BILLING_PROCESS_CONFIGURATION,
        configuration.getId(),
        EventLogger.MODULE_BILLING_PROCESS,
        EventLogger.ROW_UPDATED,
        configuration.getReviewStatus(),
        null,
        null);
    configuration.setReviewStatus(
        flag ? Constants.REVIEW_STATUS_APPROVED : Constants.REVIEW_STATUS_DISAPPROVED);
  }
  public void purgeReview(Integer entityId, boolean isReview) {
    BillingProcessDTO review = billingProcessDas.findReview(entityId);
    if (review == null) {
      // no review, nothing to delete then
      return;
    }

    // if we are here, a review exists
    ConfigurationBL configBL = new ConfigurationBL(entityId);
    if (configBL.getEntity().getGenerateReport().intValue() == 1
        && !new Integer(configBL.getEntity().getReviewStatus())
            .equals(Constants.REVIEW_STATUS_APPROVED)
        && !isReview) {
      eLogger.warning(
          entityId,
          null,
          configBL.getEntity().getId(),
          EventLogger.MODULE_BILLING_PROCESS,
          EventLogger.BILLING_REVIEW_NOT_APPROVED,
          Constants.TABLE_BILLING_PROCESS_CONFIGURATION);
    }
    // delete the review
    LOG.debug("Removing review id = " + review.getId() + " from " + " entity " + entityId);
    // this is needed while the order process is JPA, but the billing process is Entity
    OrderProcessDAS processDas = new OrderProcessDAS();
    com.sapienter.jbilling.server.process.db.BillingProcessDTO processDto =
        new BillingProcessDAS().find(review.getId());
    for (OrderProcessDTO orderDto : processDto.getOrderProcesses()) {
      processDas.delete(orderDto);
    }
    processDto.getOrderProcesses().clear();
    // delete processRunUsers otherwise will be constraint violation
    for (ProcessRunDTO processRun : review.getProcessRuns()) {
      new ProcessRunUserDAS().removeProcessRunUsersForProcessRun(processRun.getId());
    }
    // otherwise this line would cascade de delete
    getHome().delete(review);
  }
 private void init() {
   eLogger = EventLogger.getInstance();
   configurationDas = new BillingProcessConfigurationDAS();
 }
Beispiel #7
0
 private void init() {
   eLogger = EventLogger.getInstance();
   invoiceDas = new InvoiceDAS();
 }
Beispiel #8
0
  /**
   * This will remove all the records (sql delete, not just flag them). It will also update the
   * related orders if applicable
   */
  public void delete(Integer executorId) throws SessionInternalError {
    if (invoice == null) {
      throw new SessionInternalError("An invoice has to be set before delete");
    }

    // prevent a delegated Invoice from being deleted
    if (invoice.getDelegatedInvoiceId() != null && invoice.getDelegatedInvoiceId().intValue() > 0) {
      SessionInternalError sie =
          new SessionInternalError("A carried forward Invoice cannot be deleted");
      sie.setErrorMessages(
          new String[] {"InvoiceDTO,invoice,invoice.error.fkconstraint," + invoice.getId()});
      throw sie;
    }
    // start by updating purchase_order.next_billable_day if applicatble
    // for each of the orders included in this invoice
    for (OrderProcessDTO orderProcess : (Collection<OrderProcessDTO>) invoice.getOrderProcesses()) {
      OrderDTO order = orderProcess.getPurchaseOrder();
      if (order.getNextBillableDay() == null) {
        // the next billable day doesn't need updating
        if (order.getStatusId().equals(Constants.ORDER_STATUS_FINISHED)) {
          OrderBL orderBL = new OrderBL(order);
          orderBL.setStatus(null, Constants.ORDER_STATUS_ACTIVE);
        }
        continue;
      }
      // only if this invoice is the responsible for the order's
      // next billable day
      if (order.getNextBillableDay().equals(orderProcess.getPeriodEnd())) {
        order.setNextBillableDay(orderProcess.getPeriodStart());
        if (order.getStatusId().equals(Constants.ORDER_STATUS_FINISHED)) {
          OrderBL orderBL = new OrderBL(order);
          orderBL.setStatus(null, Constants.ORDER_STATUS_ACTIVE);
        }
      }
    }

    // go over the order process records again just to delete them
    // we are done with this order, delete the process row
    for (OrderProcessDTO orderProcess : (Collection<OrderProcessDTO>) invoice.getOrderProcesses()) {
      OrderDTO order = orderProcess.getPurchaseOrder();
      OrderProcessDAS das = new OrderProcessDAS();
      order.getOrderProcesses().remove(orderProcess);
      das.delete(orderProcess);
    }
    invoice.getOrderProcesses().clear();

    // get rid of the contact associated with this invoice
    try {
      ContactBL contact = new ContactBL();
      if (contact.setInvoice(invoice.getId())) {
        contact.delete();
      }
    } catch (Exception e1) {
      LOG.error("Exception deleting the contact of an invoice", e1);
    }

    // remove the payment link/s
    PaymentBL payment = new PaymentBL();
    Iterator<PaymentInvoiceMapDTO> it = invoice.getPaymentMap().iterator();
    while (it.hasNext()) {
      PaymentInvoiceMapDTO map = it.next();
      payment.removeInvoiceLink(map.getId());
      invoice.getPaymentMap().remove(map);
      // needed because the collection has changed
      it = invoice.getPaymentMap().iterator();
    }

    // log that this was deleted, otherwise there will be no trace
    if (executorId != null) {
      eLogger.audit(
          executorId,
          invoice.getBaseUser().getId(),
          Constants.TABLE_INVOICE,
          invoice.getId(),
          EventLogger.MODULE_INVOICE_MAINTENANCE,
          EventLogger.ROW_DELETED,
          null,
          null,
          null);
    }

    // before delete the invoice most delete the reference in table
    // PAYMENT_INVOICE
    new PaymentInvoiceMapDAS().deleteAllWithInvoice(invoice);

    Set<InvoiceDTO> invoices = invoice.getInvoices();
    if (invoices.size() > 0) {
      for (InvoiceDTO delegate : invoices) {
        // set status to unpaid as against carried
        delegate.setInvoiceStatus(new InvoiceStatusDAS().find(Constants.INVOICE_STATUS_UNPAID));
        // remove delegated invoice link
        delegate.setInvoice(null);
        getHome().save(delegate);
      }
    }

    // now delete the invoice itself
    getHome().delete(invoice);
    getHome().flush();
  }
Beispiel #9
0
  /**
   * @param userId
   * @param newInvoice
   * @param process It can be null.
   */
  public void create(
      Integer userId, NewInvoiceDTO newInvoice, BillingProcessDTO process, Integer executorUserId) {
    // find out the entity id
    PreferenceBL pref = new PreferenceBL();
    UserBL user = null;
    Integer entityId;
    if (process != null) {
      entityId = process.getEntity().getId();
    } else {
      // this is a manual invoice, there's no billing process
      user = new UserBL(userId);
      entityId = user.getEntityId(userId);
    }

    // verify if this entity is using the 'continuous invoice date'
    // preference
    try {
      pref.set(entityId, Constants.PREFERENCE_CONTINUOUS_DATE);

      if (StringUtils.isNotBlank(pref.getString())) {
        Date lastDate = com.sapienter.jbilling.common.Util.parseDate(pref.getString());
        LOG.debug("Last date invoiced: " + lastDate);

        if (lastDate.after(newInvoice.getBillingDate())) {
          LOG.debug(
              "Due date is before the last recorded date. Moving due date forward for continuous invoice dates.");
          newInvoice.setBillingDate(lastDate);

        } else {
          // update the lastest date only if this is not a review
          if (newInvoice.getIsReview() == null || newInvoice.getIsReview() == 0) {
            pref.createUpdateForEntity(
                entityId,
                Constants.PREFERENCE_CONTINUOUS_DATE,
                com.sapienter.jbilling.common.Util.parseDate(newInvoice.getBillingDate()));
          }
        }
      }
    } catch (EmptyResultDataAccessException e) {
      // not interested, ignore
    }

    // in any case, ensure that the due date is => that invoice date
    if (newInvoice.getDueDate().before(newInvoice.getBillingDate())) {
      LOG.debug("Due date before billing date, moving date up to billing date.");
      newInvoice.setDueDate(newInvoice.getBillingDate());
    }

    // ensure that there are only so many decimals in the invoice
    int decimals = Constants.BIGDECIMAL_SCALE;
    try {
      pref.set(entityId, Constants.PREFERENCE_INVOICE_DECIMALS);
      decimals = pref.getInt();
    } catch (EmptyResultDataAccessException e) {
      // not interested, ignore
    }

    LOG.debug("Rounding " + newInvoice.getTotal() + " to " + decimals + " decimals");
    if (newInvoice.getTotal() != null) {
      newInvoice.setTotal(newInvoice.getTotal().setScale(decimals, Constants.BIGDECIMAL_ROUND));
    }
    if (newInvoice.getBalance() != null) {
      newInvoice.setBalance(newInvoice.getBalance().setScale(decimals, Constants.BIGDECIMAL_ROUND));
    }

    // some API calls only accept ID's and do not pass meta-fields
    // update and validate meta-fields if they've been populated
    if (newInvoice.getMetaFields() != null && !newInvoice.getMetaFields().isEmpty()) {
      newInvoice.updateMetaFieldsWithValidation(entityId, newInvoice);
    }

    // create the invoice row
    invoice = invoiceDas.create(userId, newInvoice, process);

    // add delegated/included invoice links
    if (newInvoice.getIsReview() == 0) {
      for (InvoiceDTO dto : newInvoice.getInvoices()) {
        dto.setInvoice(invoice);
      }
    }

    // add the customer notes if it applies
    try {
      pref.set(entityId, Constants.PREFERENCE_SHOW_NOTE_IN_INVOICE);
    } catch (EmptyResultDataAccessException e) {
      // use the default then
    }

    if (pref.getInt() == 1) {
      if (user == null) {
        user = new UserBL(userId);
      }
      if (user.getEntity().getCustomer() != null
          && user.getEntity().getCustomer().getNotes() != null) {
        // append the notes if there's some text already there
        newInvoice.setCustomerNotes(
            (newInvoice.getCustomerNotes() == null)
                ? user.getEntity().getCustomer().getNotes()
                : newInvoice.getCustomerNotes() + " " + user.getEntity().getCustomer().getNotes());
      }
    }
    // notes might come from the customer, the orders, or both
    if (newInvoice.getCustomerNotes() != null && newInvoice.getCustomerNotes().length() > 0) {
      invoice.setCustomerNotes(newInvoice.getCustomerNotes());
    }

    // calculate/compose the number
    String numberStr = null;
    if (newInvoice.getIsReview() != null && newInvoice.getIsReview() == 1) {
      // invoices for review will be seen by the entity employees
      // so the entity locale will be used
      EntityBL entity = new EntityBL(entityId);
      ResourceBundle bundle = ResourceBundle.getBundle("entityNotifications", entity.getLocale());
      numberStr = bundle.getString("invoice.review.number");
    } else if (newInvoice.getPublicNumber() == null || newInvoice.getPublicNumber().length() == 0) {
      String prefix;
      try {
        pref.set(entityId, Constants.PREFERENCE_INVOICE_PREFIX);
        prefix = pref.getString();
        if (prefix == null) {
          prefix = "";
        }
      } catch (EmptyResultDataAccessException e) {
        prefix = "";
      }
      int number;
      try {
        pref.set(entityId, Constants.PREFERENCE_INVOICE_NUMBER);
        number = pref.getInt();
      } catch (EmptyResultDataAccessException e1) {
        number = 1;
      }

      numberStr = prefix + number;
      // update for the next time
      number++;
      pref.createUpdateForEntity(entityId, Constants.PREFERENCE_INVOICE_NUMBER, number);
    } else { // for upload of legacy invoices
      numberStr = newInvoice.getPublicNumber();
    }

    invoice.setPublicNumber(numberStr);

    // set the invoice's contact info with the current user's primary
    ContactBL contact = new ContactBL();
    contact.set(userId);
    contact.createForInvoice(contact.getDTO(), invoice.getId());

    // add a log row for convenience
    if (null != executorUserId) {
      eLogger.audit(
          executorUserId,
          userId,
          Constants.TABLE_INVOICE,
          invoice.getId(),
          EventLogger.MODULE_INVOICE_MAINTENANCE,
          EventLogger.ROW_CREATED,
          null,
          null,
          null);
    } else {
      eLogger.auditBySystem(
          entityId,
          userId,
          Constants.TABLE_INVOICE,
          invoice.getId(),
          EventLogger.MODULE_INVOICE_MAINTENANCE,
          EventLogger.ROW_CREATED,
          null,
          null,
          null);
    }
  }
Beispiel #10
0
  private boolean addOrderToInvoice(
      Integer entityId, OrderDTO order, NewInvoiceDTO newInvoice, Date processDate, int maxPeriods)
      throws SessionInternalError, TaskException, PluggableTaskException {
    // require the calculation of the period dates
    PluggableTaskManager taskManager =
        new PluggableTaskManager(entityId, Constants.PLUGGABLE_TASK_ORDER_PERIODS);
    OrderPeriodTask optask = (OrderPeriodTask) taskManager.getNextClass();

    if (optask == null) {
      throw new SessionInternalError(
          "There has to be " + "one order period pluggable task configured");
    }
    Date start = optask.calculateStart(order);
    Date end = optask.calculateEnd(order, processDate, maxPeriods, start);
    List<PeriodOfTime> periods = optask.getPeriods();
    // there isn't anything billable from this order
    if (periods.size() == 0) {
      return false;
    }

    if (start != null && end != null && start.after(end)) {
      // how come it starts after it ends ???
      throw new SessionInternalError(
          "Calculated for "
              + "order "
              + order.getId()
              + " a period that"
              + " starts after it ends:"
              + start
              + " "
              + end);
    }

    // add this order to the invoice being created
    newInvoice.addOrder(order, start, end, periods);

    // prepaid orders shouldn't have to be included
    // past time.
    if (order.getBillingTypeId().compareTo(Constants.ORDER_BILLING_PRE_PAID) == 0
        && start != null
        && // it has to be recursive too
        processDate.after(start)) {

      eLogger.warning(
          entityId,
          order.getBaseUserByUserId().getId(),
          order.getId(),
          EventLogger.MODULE_BILLING_PROCESS,
          EventLogger.BILLING_PROCESS_UNBILLED_PERIOD,
          Constants.TABLE_PUCHASE_ORDER);

      LOG.warn("Order " + order.getId() + " is prepaid " + "but has past time not billed.");
    }

    // initialize the currency of the new invoice
    if (newInvoice.getCurrency() == null) {
      newInvoice.setCurrency(order.getCurrency());
    } else {
      // now we are not supporting orders with different
      // currencies in the same invoice. Later this could be done
      if (newInvoice.getCurrency().getId() != order.getCurrency().getId()) {
        throw new SessionInternalError(
            "Orders with different "
                + "currencies not supported in one invoice. "
                + "Currency = "
                + newInvoice.getCurrency().getId()
                + "order = "
                + order.getId());
      }
    }
    return true;
  }
Beispiel #11
0
  /**
   * Generates one single invoice for one single purchase order. This is meant to be called outside
   * the billing process.
   *
   * @param orderId
   * @return
   * @throws PluggableTaskException
   * @throws SessionInternalError
   */
  public InvoiceDTO generateInvoice(Integer orderId, Integer invoiceId)
      throws PluggableTaskException, SessionInternalError, SQLException {
    InvoiceDTO retValue = null;
    // find the order
    OrderBL order = new OrderBL(orderId);
    // define some data
    Integer entityId = order.getEntity().getUser().getEntity().getId();
    ConfigurationBL config = new ConfigurationBL(entityId);
    int maxPeriods = config.getEntity().getMaximumPeriods();
    boolean paymentApplication = config.getEntity().getAutoPaymentApplication() == 1;
    // The user could be the parent of a sub-account
    Integer userId = findUserId(order.getEntity());
    Date processDate = Calendar.getInstance().getTime();
    processDate = Util.truncateDate(processDate);
    // create the my invoice
    NewInvoiceDTO newInvoice = new NewInvoiceDTO();
    newInvoice.setDate(processDate);
    newInvoice.setIsReview(new Integer(0));
    // find the due date that applies
    TimePeriod period = order.getDueDate();
    newInvoice.setDueDatePeriod(period);
    // this is an isolated invoice that doesn't care about previous
    // overdue invoices
    newInvoice.setCarriedBalance(BigDecimal.ZERO);
    newInvoice.setInvoiceStatus(new InvoiceStatusDAS().find(Constants.INVOICE_STATUS_UNPAID));

    try {
      // put the order in the invoice using all the pluggable taks stuff
      addOrderToInvoice(entityId, order.getEntity(), newInvoice, processDate, maxPeriods);

      // this means that the user is trying to generate an invoice from
      // an order that the configurated tasks have rejected. Therefore
      // either this is the case an generating this invoice doesn't make
      // sense, or some business rules in the tasks have to be changed
      // (probably with a personalized task for this entity)
      if (newInvoice.getOrders().size() == 0) {
        return null;
      }

      // process events before orders added to invoice
      processOrderToInvoiceEvents(newInvoice, entityId);

      // generate the invoice lines
      composeInvoice(entityId, userId, newInvoice);

      // process events after orders added to invoice
      processOrderAddedOnInvoiceEvents(newInvoice, entityId);

      // put the resulting invoice in the database
      if (invoiceId == null) {
        // it is a new invoice from a singe order
        retValue =
            generateDBInvoice(userId, newInvoice, null, Constants.ORDER_PROCESS_ORIGIN_MANUAL);
        // try to get this new invioce paid by previously unlinked
        // payments
        if (paymentApplication) {
          PaymentBL pBL = new PaymentBL();
          pBL.automaticPaymentApplication(retValue);
        }
      } else {
        // it is an order going into an existing invoice
        InvoiceBL invoice = new InvoiceBL(invoiceId);
        boolean isUnpaid = invoice.getEntity().getToProcess() == 1;
        invoice.update(newInvoice);
        retValue = invoice.getEntity();
        createOrderProcess(newInvoice, retValue, null, Constants.ORDER_PROCESS_ORIGIN_MANUAL);
        eLogger.info(
            entityId,
            userId,
            invoiceId,
            EventLogger.MODULE_INVOICE_MAINTENANCE,
            EventLogger.INVOICE_ORDER_APPLIED,
            Constants.TABLE_INVOICE);
        // if the invoice is now not payable, take the user
        // out of ageing
        if (isUnpaid && retValue.getToProcess() == 0) {
          AgeingBL ageing = new AgeingBL();
          ageing.out(retValue.getBaseUser(), null);
        }
      }
    } catch (TaskException e) {
      // this means that the user is trying to generate an invoice from
      // an order that the configurated tasks have rejected. Therefore
      // either this is the case an generating this invoice doesn't make
      // sense, or some business rules in the tasks have to be changed
      // (probably with a personalized task for this entity)
      LOG.warn("Exception in generate invoice ", e);
    }

    if (retValue != null) {
      InvoicesGeneratedEvent generatedEvent = new InvoicesGeneratedEvent(entityId, null);
      generatedEvent.getInvoiceIds().add(retValue.getId());
      EventManager.process(generatedEvent);
    }

    return retValue;
  }
/**
 * BasicAgeingTask
 *
 * @author Brian Cowdery
 * @since 28/04/11
 */
public class BasicAgeingTask extends PluggableTask implements IAgeingTask {

  private static final Logger LOG = Logger.getLogger(BasicAgeingTask.class);
  private final EventLogger eLogger = EventLogger.getInstance();

  private static Calendar calendar = GregorianCalendar.getInstance();

  static {
    calendar.clear();
  }

  private Map<Integer, Integer> gracePeriodCache = new HashMap<Integer, Integer>();

  protected int getGracePeriod(Integer entityId) {
    if (!gracePeriodCache.containsKey(entityId)) {
      PreferenceBL preference = new PreferenceBL();
      preference.set(entityId, Constants.PREFERENCE_GRACE_PERIOD);
      gracePeriodCache.put(entityId, preference.getInt());
    }

    return gracePeriodCache.get(entityId);
  }

  /**
   * Review all users for the given day, and age those that have outstanding invoices over the set
   * number of days for an ageing step.
   *
   * @param steps ageing steps
   * @param today today's date
   * @param executorId executor id
   */
  public void reviewAllUsers(
      Integer entityId, Set<AgeingEntityStepDTO> steps, Date today, Integer executorId) {
    LOG.debug("Reviewing users for entity " + entityId + " ...");

    // go over all the users already in the ageing system
    for (UserDTO user : new UserDAS().findAgeing(entityId)) {
      ageUser(steps, user, today, executorId);
    }

    // go over the active users with payable invoices
    try {
      UserDAS userDas = new UserDAS();
      InvoiceDAS invoiceDas = new InvoiceDAS();

      CachedRowSet users = new UserBL().findActiveWithOpenInvoices(entityId);

      while (users.next()) {
        Integer userId = users.getInt(1);
        UserDTO user = userDas.find(userId);
        int gracePeriod = getGracePeriod(entityId);

        LOG.debug(
            "Reviewing invoices for user "
                + user.getId()
                + " using a grace period of "
                + gracePeriod
                + " days.");

        for (InvoiceDTO invoice : invoiceDas.findProccesableByUser(user)) {
          if (isInvoiceOverdue(invoice, user, gracePeriod, today)) {
            ageUser(steps, user, today, executorId);
            break;
          }
        }
      }

    } catch (SQLException e) {
      LOG.error("Failed to fetch users with payable invoices.", e);

    } catch (NamingException e) {
      LOG.error("Exception fetching users with payable invoices.", e);
    }
  }

  /**
   * Moves a user one step forward in the ageing process (move from active -> suspended etc.). The
   * user will only be moved if they have spent long enough in their present status.
   *
   * @param steps ageing steps
   * @param user user to age
   * @param today today's date
   * @return the resulting ageing step for the user after ageing
   */
  public AgeingEntityStepDTO ageUser(
      Set<AgeingEntityStepDTO> steps, UserDTO user, Date today, Integer executorId) {
    LOG.debug("Ageing user " + user.getId());

    Integer currentStatusId = user.getStatus().getId();
    UserStatusDTO nextStatus = null;
    AgeingEntityStepDTO ageingStep = null;

    if (currentStatusId.equals(UserDTOEx.STATUS_ACTIVE)) {
      // send welcome message (initial step after active).
      nextStatus = getNextAgeingStep(steps, UserDTOEx.STATUS_ACTIVE);

    } else {
      // user already in the ageing process
      ageingStep = new AgeingEntityStepDAS().findStep(user.getEntity().getId(), currentStatusId);

      if (ageingStep != null) {
        // determine the next ageing step
        if (isAgeingRequired(user, ageingStep, today)) {
          nextStatus = getNextAgeingStep(steps, currentStatusId);
          LOG.debug(
              "User "
                  + user.getId()
                  + " needs to be aged to '"
                  + getStatusDescription(nextStatus)
                  + "'");
        }

      } else {
        // User is in a non-existent ageing status... Either the status was removed or
        // the data is bad. As a workaround, just move to the next status.
        nextStatus = getNextAgeingStep(steps, currentStatusId);
        LOG.warn(
            "User "
                + user.getId()
                + " is in an invalid ageing step. Moving to '"
                + getStatusDescription(nextStatus)
                + "'");
      }
    }

    // set status
    if (nextStatus != null) {
      setUserStatus(user, nextStatus, today, null);

    } else {
      LOG.debug("Next status is null, no further ageing steps are available.");
      eLogger.warning(
          user.getEntity().getId(),
          user.getUserId(),
          user.getUserId(),
          EventLogger.MODULE_USER_MAINTENANCE,
          EventLogger.NO_FURTHER_STEP,
          Constants.TABLE_BASE_USER);
    }

    return ageingStep;
  }

  /**
   * Returns true if the given invoice is overdue.
   *
   * @param invoice invoice to check
   * @param user user owning the invoice
   * @param gracePeriod company wide grace period
   * @param today today's date
   * @return true if invoice is overdue, false if not
   */
  public boolean isInvoiceOverdue(
      InvoiceDTO invoice, UserDTO user, Integer gracePeriod, Date today) {
    calendar.clear();
    calendar.setTime(invoice.getDueDate());
    calendar.add(Calendar.DATE, gracePeriod);

    if (calendar.getTime().before(today)) {
      LOG.debug(
          "Invoice is overdue (due date "
              + invoice.getDueDate()
              + " + "
              + gracePeriod
              + " days grace, is before today "
              + today
              + ")");
      return true;
    }

    LOG.debug(
        "Invoice is NOT overdue (due date "
            + invoice.getDueDate()
            + " + "
            + gracePeriod
            + " days grace is after today "
            + today
            + ")");
    return false;
  }

  /**
   * Returns true if the user requires ageing.
   *
   * @param user user being reviewed
   * @param currentStep current ageing step of the user
   * @param today today's date
   * @return true if user requires ageing, false if not
   */
  public boolean isAgeingRequired(UserDTO user, AgeingEntityStepDTO currentStep, Date today) {
    Date lastStatusChange =
        user.getLastStatusChange() != null ? user.getLastStatusChange() : user.getCreateDatetime();

    calendar.clear();
    calendar.setTime(lastStatusChange);
    calendar.add(Calendar.DATE, currentStep.getDays());

    if (calendar.getTime().equals(today) || calendar.getTime().before(today)) {
      LOG.debug(
          "User status has expired (last change "
              + lastStatusChange
              + " + "
              + currentStep.getDays()
              + " days is before today "
              + today
              + ")");
      return true;
    }

    LOG.debug(
        "User does not need to be aged (last change "
            + lastStatusChange
            + " + "
            + currentStep.getDays()
            + " days is after today "
            + today
            + ")");
    return false;
  }

  /**
   * Removes a user from the ageing process (makes them active), ONLY if they do not still have
   * overdue invoices.
   *
   * @param user user to make active
   * @param excludedInvoiceId invoice id to ignore when determining if the user CAN be made active
   * @param executorId executor id
   */
  public void removeUser(UserDTO user, Integer executorId, Integer excludedInvoiceId) {
    Date now = new Date();

    // validate that the user actually needs a status change
    if (user.getStatus().getId() != UserDTOEx.STATUS_ACTIVE) {
      LOG.debug("User " + user.getId() + " is already active, no need to remove from ageing.");
      return;
    }

    // validate that the user does not still have overdue invoices
    try {
      if (new InvoiceBL().isUserWithOverdueInvoices(user.getUserId(), now, excludedInvoiceId)) {
        LOG.debug(
            "User " + user.getId() + " still has overdue invoices, cannot remove from ageing.");
        return;
      }
    } catch (SQLException e) {
      LOG.error("Exception occurred checking for overdue invoices.", e);
      return;
    }

    // make the status change.
    LOG.debug("Removing user " + user.getUserId() + " from ageing (making active).");
    UserStatusDTO status = new UserStatusDAS().find(UserDTOEx.STATUS_ACTIVE);
    setUserStatus(user, status, now, null);
  }

  /**
   * Sets the user status to the given "aged" status. If the user status is already set to the aged
   * status no changes will be made. This method also performs an HTTP callback and sends a
   * notification message when a status change is made.
   *
   * <p>If the user becomes suspended and can no longer log-in to the system, all of their active
   * orders will be automatically suspended.
   *
   * <p>If the user WAS suspended and becomes active (and can now log-in to the system), any
   * automatically suspended orders will be re-activated.
   *
   * @param user user
   * @param status status to set
   * @param today today's date
   * @param executorId executor id
   */
  public void setUserStatus(UserDTO user, UserStatusDTO status, Date today, Integer executorId) {
    // only set status if the new "aged" status is different from the users current status
    if (status.getId() == user.getStatus().getId()) {
      return;
    }

    LOG.debug("Setting user " + user.getId() + " status to '" + getStatusDescription(status) + "'");

    if (executorId != null) {
      // this came from the gui
      eLogger.audit(
          executorId,
          user.getId(),
          Constants.TABLE_BASE_USER,
          user.getId(),
          EventLogger.MODULE_USER_MAINTENANCE,
          EventLogger.STATUS_CHANGE,
          user.getStatus().getId(),
          null,
          null);
    } else {
      // this is from a process, no executor involved
      eLogger.auditBySystem(
          user.getCompany().getId(),
          user.getId(),
          Constants.TABLE_BASE_USER,
          user.getId(),
          EventLogger.MODULE_USER_MAINTENANCE,
          EventLogger.STATUS_CHANGE,
          user.getStatus().getId(),
          null,
          null);
    }

    // make the change
    boolean couldLogin = user.getStatus().getCanLogin() == 1;
    UserStatusDTO oldStatus = user.getStatus();

    user.setUserStatus(status);
    user.setLastStatusChange(today);

    // status changed to deleted, remove user
    if (status.getId() == UserDTOEx.STATUS_DELETED) {
      LOG.debug("Deleting user " + user.getId());
      new UserBL(user.getId()).delete(executorId);
      return;
    }

    // status changed from active to suspended
    // suspend customer orders
    if (couldLogin && status.getCanLogin() == 0) {
      LOG.debug("User " + user.getId() + " cannot log-in to the system. Suspending active orders.");

      OrderBL orderBL = new OrderBL();
      ScrollableResults orders =
          new OrderDAS().findByUser_Status(user.getId(), Constants.ORDER_STATUS_ACTIVE);

      while (orders.next()) {
        OrderDTO order = (OrderDTO) orders.get()[0];
        orderBL.set(order);
        orderBL.setStatus(executorId, Constants.ORDER_STATUS_SUSPENDED_AGEING);
      }

      orders.close();
    }

    // status changed from suspended to active
    // re-active suspended customer orders
    if (!couldLogin && status.getCanLogin() == 1) {
      LOG.debug(
          "User "
              + user.getId()
              + " can now log-in to the system. Activating previously suspended orders.");

      OrderBL orderBL = new OrderBL();
      ScrollableResults orders =
          new OrderDAS().findByUser_Status(user.getId(), Constants.ORDER_STATUS_SUSPENDED_AGEING);

      while (orders.next()) {
        OrderDTO order = (OrderDTO) orders.get()[0];
        orderBL.set(order);
        orderBL.setStatus(executorId, Constants.ORDER_STATUS_ACTIVE);
      }

      orders.close();
    }

    // perform callbacks and notifications
    performAgeingCallback(user, oldStatus, status);
    sendAgeingNotification(user, oldStatus, status);

    // emit NewUserStatusEvent
    NewUserStatusEvent event =
        new NewUserStatusEvent(
            user.getCompany().getId(), user.getId(), oldStatus.getId(), status.getId());
    EventManager.process(event);
  }

  protected boolean performAgeingCallback(
      UserDTO user, UserStatusDTO oldStatus, UserStatusDTO newStatus) {
    String url = null;
    try {
      PreferenceBL pref = new PreferenceBL();
      pref.set(user.getEntity().getId(), Constants.PREFERENCE_URL_CALLBACK);
      url = pref.getString();

    } catch (EmptyResultDataAccessException e) {
      /* ignore, no callback preference configured */
    }

    if (url != null && url.length() > 0) {
      try {
        LOG.debug("Performing ageing HTTP callback for URL: " + url);

        // cook the parameters to be sent
        NameValuePair[] data = new NameValuePair[6];
        data[0] = new NameValuePair("cmd", "ageing_update");
        data[1] = new NameValuePair("user_id", String.valueOf(user.getId()));
        data[2] = new NameValuePair("login_name", user.getUserName());
        data[3] = new NameValuePair("from_status", String.valueOf(oldStatus.getId()));
        data[4] = new NameValuePair("to_status", String.valueOf(newStatus.getId()));
        data[5] = new NameValuePair("can_login", String.valueOf(newStatus.getCanLogin()));

        // make the call
        HttpClient client = new HttpClient();
        client.setConnectionTimeout(30000);
        PostMethod post = new PostMethod(url);
        post.setRequestBody(data);
        client.executeMethod(post);

      } catch (Exception e) {
        LOG.error("Exception occurred posting ageing HTTP callback for URL: " + url, e);
        return false;
      }
    }
    return true;
  }

  protected boolean sendAgeingNotification(
      UserDTO user, UserStatusDTO oldStatus, UserStatusDTO newStatus) {
    try {
      MessageDTO message =
          new NotificationBL()
              .getAgeingMessage(
                  user.getEntity().getId(),
                  user.getLanguage().getId(),
                  newStatus.getId(),
                  user.getId());

      INotificationSessionBean notification =
          (INotificationSessionBean) Context.getBean(Context.Name.NOTIFICATION_SESSION);
      notification.notify(user, message);

    } catch (NotificationNotFoundException e) {
      LOG.warn(
          "Failed to send ageing notification. Entity "
              + user.getEntity().getId()
              + " does not have an ageing message configured for status '"
              + getStatusDescription(newStatus)
              + "'.");
      return false;
    }
    return true;
  }

  /**
   * Get the status for the next step in the ageing process, based on the users current status.
   *
   * @param steps configured ageing steps
   * @param currentStatusId the current user status
   */
  public UserStatusDTO getNextAgeingStep(Set<AgeingEntityStepDTO> steps, Integer currentStatusId) {
    for (AgeingEntityStepDTO step : steps) {
      Integer stepStatusId = step.getUserStatus().getId();
      if (stepStatusId.compareTo(currentStatusId) > 0) {
        return step.getUserStatus();
      }
    }

    return null;
  }

  /**
   * Null safe convenience method to return the status description.
   *
   * @param status user status
   * @return description
   */
  private String getStatusDescription(UserStatusDTO status) {
    if (status != null) {
      return status.getDescription();
    }
    return null;
  }
}
  /**
   * Sets the user status to the given "aged" status. If the user status is already set to the aged
   * status no changes will be made. This method also performs an HTTP callback and sends a
   * notification message when a status change is made.
   *
   * <p>If the user becomes suspended and can no longer log-in to the system, all of their active
   * orders will be automatically suspended.
   *
   * <p>If the user WAS suspended and becomes active (and can now log-in to the system), any
   * automatically suspended orders will be re-activated.
   *
   * @param user user
   * @param status status to set
   * @param today today's date
   * @param executorId executor id
   */
  public void setUserStatus(UserDTO user, UserStatusDTO status, Date today, Integer executorId) {
    // only set status if the new "aged" status is different from the users current status
    if (status.getId() == user.getStatus().getId()) {
      return;
    }

    LOG.debug("Setting user " + user.getId() + " status to '" + getStatusDescription(status) + "'");

    if (executorId != null) {
      // this came from the gui
      eLogger.audit(
          executorId,
          user.getId(),
          Constants.TABLE_BASE_USER,
          user.getId(),
          EventLogger.MODULE_USER_MAINTENANCE,
          EventLogger.STATUS_CHANGE,
          user.getStatus().getId(),
          null,
          null);
    } else {
      // this is from a process, no executor involved
      eLogger.auditBySystem(
          user.getCompany().getId(),
          user.getId(),
          Constants.TABLE_BASE_USER,
          user.getId(),
          EventLogger.MODULE_USER_MAINTENANCE,
          EventLogger.STATUS_CHANGE,
          user.getStatus().getId(),
          null,
          null);
    }

    // make the change
    boolean couldLogin = user.getStatus().getCanLogin() == 1;
    UserStatusDTO oldStatus = user.getStatus();

    user.setUserStatus(status);
    user.setLastStatusChange(today);

    // status changed to deleted, remove user
    if (status.getId() == UserDTOEx.STATUS_DELETED) {
      LOG.debug("Deleting user " + user.getId());
      new UserBL(user.getId()).delete(executorId);
      return;
    }

    // status changed from active to suspended
    // suspend customer orders
    if (couldLogin && status.getCanLogin() == 0) {
      LOG.debug("User " + user.getId() + " cannot log-in to the system. Suspending active orders.");

      OrderBL orderBL = new OrderBL();
      ScrollableResults orders =
          new OrderDAS().findByUser_Status(user.getId(), Constants.ORDER_STATUS_ACTIVE);

      while (orders.next()) {
        OrderDTO order = (OrderDTO) orders.get()[0];
        orderBL.set(order);
        orderBL.setStatus(executorId, Constants.ORDER_STATUS_SUSPENDED_AGEING);
      }

      orders.close();
    }

    // status changed from suspended to active
    // re-active suspended customer orders
    if (!couldLogin && status.getCanLogin() == 1) {
      LOG.debug(
          "User "
              + user.getId()
              + " can now log-in to the system. Activating previously suspended orders.");

      OrderBL orderBL = new OrderBL();
      ScrollableResults orders =
          new OrderDAS().findByUser_Status(user.getId(), Constants.ORDER_STATUS_SUSPENDED_AGEING);

      while (orders.next()) {
        OrderDTO order = (OrderDTO) orders.get()[0];
        orderBL.set(order);
        orderBL.setStatus(executorId, Constants.ORDER_STATUS_ACTIVE);
      }

      orders.close();
    }

    // perform callbacks and notifications
    performAgeingCallback(user, oldStatus, status);
    sendAgeingNotification(user, oldStatus, status);

    // emit NewUserStatusEvent
    NewUserStatusEvent event =
        new NewUserStatusEvent(
            user.getCompany().getId(), user.getId(), oldStatus.getId(), status.getId());
    EventManager.process(event);
  }