com.sapienter.jbilling.server.process.BillingProcessSessionBean.java Source code

Java tutorial

Introduction

Here is the source code for com.sapienter.jbilling.server.process.BillingProcessSessionBean.java

Source

/*
 jBilling - The Enterprise Open Source Billing System
 Copyright (C) 2003-2011 Enterprise jBilling Software Ltd. and Emiliano Conde
    
 This file is part of jbilling.
    
 jbilling is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.
    
 jbilling is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU Affero General Public License for more details.
    
 You should have received a copy of the GNU Affero General Public License
 along with jbilling.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.sapienter.jbilling.server.process;

import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.log4j.Logger;

import org.hibernate.ScrollableResults;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import com.sapienter.jbilling.common.SessionInternalError;
import com.sapienter.jbilling.common.Util;
import com.sapienter.jbilling.server.invoice.InvoiceBL;
import com.sapienter.jbilling.server.invoice.PaperInvoiceBatchBL;
import com.sapienter.jbilling.server.invoice.db.InvoiceDAS;
import com.sapienter.jbilling.server.invoice.db.InvoiceDTO;
import com.sapienter.jbilling.server.notification.INotificationSessionBean;
import com.sapienter.jbilling.server.notification.MessageDTO;
import com.sapienter.jbilling.server.notification.NotificationBL;
import com.sapienter.jbilling.server.notification.NotificationNotFoundException;
import com.sapienter.jbilling.server.payment.event.EndProcessPaymentEvent;
import com.sapienter.jbilling.server.payment.event.ProcessPaymentEvent;
import com.sapienter.jbilling.server.pluggableTask.admin.PluggableTaskManager;
import com.sapienter.jbilling.server.process.db.*;
import com.sapienter.jbilling.server.process.event.NoNewInvoiceEvent;
import com.sapienter.jbilling.server.process.task.BasicBillingProcessFilterTask;
import com.sapienter.jbilling.server.process.task.IBillingProcessFilterTask;
import com.sapienter.jbilling.server.system.event.EventManager;
import com.sapienter.jbilling.server.user.EntityBL;
import com.sapienter.jbilling.server.user.UserBL;
import com.sapienter.jbilling.server.user.db.CompanyDAS;
import com.sapienter.jbilling.server.user.db.CompanyDTO;
import com.sapienter.jbilling.server.util.Constants;
import com.sapienter.jbilling.server.util.Context;
import com.sapienter.jbilling.server.util.MapPeriodToCalendar;
import com.sapienter.jbilling.server.util.PreferenceBL;
import com.sapienter.jbilling.server.util.audit.EventLogger;

/**
 *
 * This is the session facade for the all the billing process and its 
 * related services. 
 */
@Transactional(propagation = Propagation.REQUIRED)
public class BillingProcessSessionBean implements IBillingProcessSessionBean {

    private static final Logger LOG = Logger.getLogger(BillingProcessSessionBean.class);

    private static final AtomicBoolean running = new AtomicBoolean(false);

    /**
     * Gets the invoices for the specified process id. The returned collection
     * is of extended dtos (InvoiceDTO).
     * @param processId
     * @return A collection of InvoiceDTO objects
     * @throws SessionInternalError
     */
    public Collection getGeneratedInvoices(Integer processId) {
        // find the billing_process home interface
        BillingProcessDAS processHome = new BillingProcessDAS();
        Collection<InvoiceDTO> invoices = new InvoiceDAS().findByProcess(processHome.find(processId));

        for (InvoiceDTO invoice : invoices) {
            invoice.getOrderProcesses().iterator().next().getId(); // it is a touch
        }

        return invoices;

    }

    /**
     * @param entityId
     * @param languageId
     * @return
     * @throws SessionInternalError
     */
    public AgeingDTOEx[] getAgeingSteps(Integer entityId, Integer executorLanguageId, Integer languageId)
            throws SessionInternalError {
        try {
            AgeingBL ageing = new AgeingBL();
            return ageing.getSteps(entityId, executorLanguageId, languageId);
        } catch (Exception e) {
            throw new SessionInternalError(e);
        }
    }

    /**
     * @param entityId
     * @param languageId
     * @param steps
     * @throws SessionInternalError
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void setAgeingSteps(Integer entityId, Integer languageId, AgeingDTOEx[] steps)
            throws SessionInternalError {
        try {
            AgeingBL ageing = new AgeingBL();
            ageing.setSteps(entityId, languageId, steps);
        } catch (Exception e) {
            throw new SessionInternalError(e);
        }
    }

    public void generateReview(Integer entityId, Date billingDate, Integer periodType, Integer periodValue)
            throws SessionInternalError {
        LOG.debug("Generating review entity " + entityId);
        IBillingProcessSessionBean local = (IBillingProcessSessionBean) Context
                .getBean(Context.Name.BILLING_PROCESS_SESSION);
        local.processEntity(entityId, billingDate, periodType, periodValue, true);
        // let know this entity that a new reivew is now pending approval
        try {
            String params[] = new String[1];
            params[0] = entityId.toString();
            NotificationBL.sendSapienterEmail(entityId, "process.new_review", null, params);
        } catch (Exception e) {
            LOG.warn("Exception sending email to entity", e);
        }
    }

    /**
     * Creates the billing process record. This has to be done in its own
     * transaction (thus, in its own method), so new invoices can link to
     * an existing process record in the db.
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer createProcessRecord(Integer entityId, Date billingDate, Integer periodType, Integer periodValue,
            boolean isReview, Integer retries) throws SQLException {
        BillingProcessBL bpBL = new BillingProcessBL();
        BillingProcessDTO dto = new BillingProcessDTO();

        // process can't leave reviews behind, and a review has to 
        // delete the previous one too            
        bpBL.purgeReview(entityId, isReview);

        //I need to find the entity
        CompanyDAS comDas = new CompanyDAS();
        CompanyDTO company = comDas.find(entityId);
        //I need to find the PeriodUnit
        PeriodUnitDAS periodDas = new PeriodUnitDAS();
        PeriodUnitDTO period = periodDas.find(periodType);

        dto.setEntity(company);
        dto.setBillingDate(Util.truncateDate(billingDate));
        dto.setPeriodUnit(period);
        dto.setPeriodValue(periodValue);
        dto.setIsReview(isReview ? new Integer(1) : new Integer(0));
        dto.setRetriesToDo(retries);

        bpBL.findOrCreate(dto);
        return bpBL.getEntity().getId();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer createRetryRun(Integer processId) {
        BillingProcessBL process = new BillingProcessBL(processId);
        // create a new run record
        BillingProcessRunBL runBL = new BillingProcessRunBL();
        runBL.create(process.getEntity(), process.getEntity().getBillingDate());
        LOG.debug("created process run " + runBL.getEntity().getId());

        return runBL.getEntity().getId();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processEntity(Integer entityId, Date billingDate, Integer periodType, Integer periodValue,
            boolean isReview) throws SessionInternalError {

        if (entityId == null || billingDate == null) {
            throw new SessionInternalError("entityId and billingDate can't be null");
        }

        try {
            ConfigurationBL conf = new ConfigurationBL(entityId);

            IBillingProcessSessionBean local = (IBillingProcessSessionBean) Context
                    .getBean(Context.Name.BILLING_PROCESS_SESSION);

            Integer billingProcessId = local.createProcessRecord(entityId, billingDate, periodType, periodValue,
                    isReview, conf.getEntity().getRetries());

            BillingProcessRunBL billingProcessRunBL = new BillingProcessRunBL();
            billingProcessRunBL.setProcess(billingProcessId);
            // TODO: all the customer's id in memory is not a good idea. 1M customers would be 4MB of memory
            List<Integer> successfullUsers = billingProcessRunBL.findSuccessfullUsers();

            // start processing users of this entity
            int totalInvoices = 0;

            boolean onlyRecurring;
            // find out parameters from the configuration
            onlyRecurring = conf.getEntity().getOnlyRecurring() == 1;
            LOG.debug("**** ENTITY " + entityId + " PROCESSING USERS");

            //Load the pluggable task for filtering the users
            PluggableTaskManager taskManager = new PluggableTaskManager(entityId,
                    Constants.PLUGGABLE_TASK_BILL_PROCESS_FILTER);

            IBillingProcessFilterTask task = (IBillingProcessFilterTask) taskManager.getNextClass();

            // If one was not configured just use the basic task by default
            if (task == null) {
                task = new BasicBillingProcessFilterTask();
            }

            BillingProcessDAS bpDas = new BillingProcessDAS();

            int usersFailed = 0;
            ScrollableResults userCursor = task.findUsersToProcess(entityId, billingDate);
            if (userCursor != null) {
                int count = 0;
                while (userCursor.next()) {
                    Integer userId = (Integer) userCursor.get(0);
                    if (successfullUsers.contains(userId)) { // TODO: change this by a query to the DB
                        LOG.debug("User #" + userId + " was successfully processed during previous run. Skipping.");
                        continue;
                    }

                    Integer result[] = null;
                    try {
                        result = local.processUser(billingProcessId, userId, isReview, onlyRecurring);
                    } catch (Throwable ex) {
                        LOG.error("Exception was caught when processing User #" + userId
                                + ". Continue process skipping user    .", ex);
                        local.addProcessRunUser(billingProcessId, userId, ProcessRunUserDTO.STATUS_FAILED);
                    }
                    if (result != null) {
                        LOG.debug("User " + userId + " done invoice generation.");
                        if (!isReview) {
                            for (int f = 0; f < result.length; f++) {
                                local.emailAndPayment(entityId, result[f], billingProcessId,
                                        conf.getEntity().getAutoPayment().intValue() == 1);
                            }
                            LOG.debug("User " + userId + " done email & payment.");
                        }
                        totalInvoices += result.length;
                        local.addProcessRunUser(billingProcessId, userId, ProcessRunUserDTO.STATUS_SUCCEEDED);
                    } else {
                        LOG.debug("User " + userId + " NOT done");
                        local.addProcessRunUser(billingProcessId, userId, ProcessRunUserDTO.STATUS_FAILED);

                        ++usersFailed;
                    }

                    // make sure the memory doesn't get flooded
                    if (++count % Constants.HIBERNATE_BATCH_SIZE == 0) {
                        bpDas.reset();
                    }
                }
                userCursor.close(); // done with the cursor, needs manual closing
            }
            // restore the configuration in the session, the reset removed it
            conf.set(entityId);

            if (usersFailed == 0) { // only if all got well processed
                // if some of the invoices were paper invoices, a new file with all
                // of them has to be generated
                try {
                    BillingProcessBL process = new BillingProcessBL(billingProcessId);
                    PaperInvoiceBatchDTO batch = process.getEntity().getPaperInvoiceBatch();
                    if (totalInvoices > 0 && batch != null) {
                        PaperInvoiceBatchBL batchBl = new PaperInvoiceBatchBL(batch);
                        batchBl.compileInvoiceFilesForProcess(entityId);

                        // send the file as an attachment 
                        batchBl.sendEmail();
                    }
                } catch (Exception e) {
                    LOG.error("Error generetaing batch file", e);
                }
                // now update the billing proces record 
            }

            if (usersFailed == 0) {
                Integer processRunId = local.updateProcessRunFinished(billingProcessId,
                        Constants.PROCESS_RUN_STATUS_SUCCESS);

                if (!isReview) {
                    // the payment processing is happening in parallel
                    // this event marks the end of it
                    EndProcessPaymentEvent event = new EndProcessPaymentEvent(processRunId, entityId);
                    EventManager.process(event);
                    // and finally the next run date in the config
                    GregorianCalendar cal = new GregorianCalendar();
                    cal.setTime(billingDate);
                    cal.add(MapPeriodToCalendar.map(periodType), periodValue.intValue());
                    conf.getEntity().setNextRunDate(cal.getTime());
                    LOG.debug("Updated run date to " + cal.getTime());
                }
            } else {
                local.updateProcessRunFinished(billingProcessId, Constants.PROCESS_RUN_STATUS_FAILED);
                billingProcessRunBL.notifyProcessRunFailure(entityId, usersFailed);

                // TODO: check, if updating totals needed
                // TODO: in the case of errors during users processing
                BillingProcessRunBL runBL = new BillingProcessRunBL();
                runBL.setProcess(billingProcessId);
                // update the totals
                runBL.updateTotals(billingProcessId);
            }

            LOG.debug("**** ENTITY " + entityId + " DONE. Failed users = " + usersFailed);
            // TODO: review that this is not needed: EventManager.process(generatedEvent);
        } catch (Exception e) {
            // no need to specify a rollback, an error in any of the
            // updates would not require the rest to be rolled back.
            // Actually, it's better to keep as far as it went.
            LOG.error("Error processing entity " + entityId, e);
        }
    }

    /**
     * This method process a payment synchronously. It is a wrapper to the payment processing  
     * so it runs in its own transaction
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processPayment(Integer processId, Integer runId, Integer invoiceId) {
        try {
            BillingProcessBL bl = new BillingProcessBL();
            bl.generatePayment(processId, runId, invoiceId);
        } catch (Exception e) {
            LOG.error("Exception processing a payment ", e);
        }
    }

    /**
     * This method marks the end of payment processing. It is a wrapper
     * so it runs in its own transaction
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void endPayments(Integer runId) {
        BillingProcessRunBL run = new BillingProcessRunBL(runId);
        run.updatePaymentsFinished();
        // update the totals
        run.updateTotals(run.getEntity().getBillingProcess().getId());
        run.updatePaymentsStatistic(run.getEntity().getId());
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean verifyIsRetry(Integer processId, int retryDays, Date today) {
        GregorianCalendar cal = new GregorianCalendar();
        // find the last run date
        BillingProcessBL process = new BillingProcessBL(processId);
        BillingProcessRunBL runBL = new BillingProcessRunBL();
        ProcessRunDTO lastRun = (ProcessRunDTO) Collections.max(process.getEntity().getProcessRuns(),
                runBL.new DateComparator());
        cal.setTime(Util.truncateDate(lastRun.getStarted()));
        LOG.debug("Retry evaluation lastrun = " + cal.getTime());
        cal.add(GregorianCalendar.DAY_OF_MONTH, retryDays);
        LOG.debug("Added days = " + cal.getTime() + " today = " + today);
        if (!cal.getTime().after(today)) {
            return true;
        } else {
            return false;
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void doRetry(Integer processId, int retryDays, Date today) throws SessionInternalError {
        try {
            IBillingProcessSessionBean process = (IBillingProcessSessionBean) Context
                    .getBean(Context.Name.BILLING_PROCESS_SESSION);

            if (process.verifyIsRetry(processId, retryDays, today)) {
                // it's time for a retry
                LOG.debug("Retring process " + processId);
                Integer runId = process.createRetryRun(processId);
                Integer entityId = new BillingProcessDAS().find(processId).getEntity().getId();

                // get the invoices yet to be paid from this process
                InvoiceBL invoiceBL = new InvoiceBL();
                for (Iterator it = invoiceBL.getHome().findProccesableByProcess(processId).iterator(); it
                        .hasNext();) {
                    InvoiceDTO invoice = (InvoiceDTO) it.next();
                    LOG.debug("Retrying invoice " + invoice.getId());

                    // post the need of a payment process, it'll be done asynchronusly
                    ProcessPaymentEvent event = new ProcessPaymentEvent(invoice.getId(), null, runId, entityId);
                    EventManager.process(event);
                }

                // update the end date of this run
                BillingProcessRunBL runBl = new BillingProcessRunBL(runId);
                runBl.updateFinished(Constants.PROCESS_RUN_STATUS_SUCCESS);

                // the payment processing is happening in parallel
                // this event marks the end of it
                EndProcessPaymentEvent event = new EndProcessPaymentEvent(runId, entityId);
                EventManager.process(event);

                // update the process: one less retry to do
                BillingProcessBL bl = new BillingProcessBL(processId);
                int now = bl.getEntity().getRetriesToDo();
                now--;
                bl.getEntity().setRetriesToDo(new Integer(now));
            }

        } catch (Exception e) {
            throw new SessionInternalError(e);
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void emailAndPayment(Integer entityId, Integer invoiceId, Integer processId, boolean processPayment) {
        try {
            InvoiceBL invoice = new InvoiceBL(invoiceId);
            Integer userId = invoice.getEntity().getBaseUser().getUserId();

            LOG.debug("email and payment for user " + userId + " invoice " + invoiceId);

            // last but not least, let this user know about his/her new
            // invoice.
            NotificationBL notif = new NotificationBL();

            try {
                MessageDTO[] invoiceMessage = notif.getInvoiceMessages(entityId, processId,
                        invoice.getEntity().getBaseUser().getLanguageIdField(), invoice.getEntity());

                INotificationSessionBean notificationSess = (INotificationSessionBean) Context
                        .getBean(Context.Name.NOTIFICATION_SESSION);

                for (int msg = 0; msg < invoiceMessage.length; msg++) {
                    notificationSess.notify(userId, invoiceMessage[msg]);
                }
            } catch (NotificationNotFoundException e) {
                LOG.warn("Invoice message not defined for entity " + entityId + " Invoice email not sent");
            }

            if (processPayment) {
                // when the preference is set, 
                // only process payment if it doesn't have a negative balance 
                // that wasn't caused by a carried balance
                InvoiceDTO dto = invoice.getDTO();
                if (BigDecimal.ZERO.compareTo(dto.getBalance()) > 0
                        && BigDecimal.ZERO.compareTo(dto.getCarriedBalance()) <= 0) {

                    PreferenceBL preferenceBL = new PreferenceBL();
                    try {
                        preferenceBL.set(entityId, Constants.PREFERENCE_DELAY_NEGATIVE_PAYMENTS);
                    } catch (EmptyResultDataAccessException fe) {
                        /* use default */ }

                    if (preferenceBL.getInt() == 1) {
                        processPayment = false;
                        LOG.warn("Delaying invoice payment with negative balance and no negative carried balance");
                    }
                }

                if (processPayment && BigDecimal.ZERO.compareTo(dto.getBalance()) != 0) {
                    ProcessPaymentEvent event = new ProcessPaymentEvent(invoiceId, processId, null, entityId);
                    EventManager.process(event);
                } else {
                    LOG.debug("Not processing a payment, balance of invoice is " + dto.getBalance());
                }
            }
        } catch (Exception e) {
            LOG.error("sending email and processing payment", e);
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }

    /**
     * Process a user, generating the invoice/s,
     * @param userId
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer[] processUser(Integer processId, Integer userId, boolean isReview, boolean onlyRecurring) {
        int invoiceGenerated = 0;
        Integer[] retValue = null;

        try {
            UserBL user = new UserBL(userId);

            if (!user.canInvoice()) {
                LOG.debug("Skipping non-customer / subaccount user " + userId);
                return new Integer[0];
            }

            BillingProcessBL processBL = new BillingProcessBL(processId);
            BillingProcessDTO process = processBL.getEntity();

            // payment and notification only needed if this user gets a 
            // new invoice.
            InvoiceDTO newInvoices[] = processBL.generateInvoice(process, user.getEntity(), isReview,
                    onlyRecurring);
            if (newInvoices == null) {
                if (!isReview) {
                    NoNewInvoiceEvent event = new NoNewInvoiceEvent(user.getEntityId(userId), userId,
                            process.getBillingDate(), user.getEntity().getSubscriberStatus().getId());
                    EventManager.process(event);
                }
                return new Integer[0];
            }

            retValue = new Integer[newInvoices.length];
            for (int f = 0; f < newInvoices.length; f++) {
                retValue[f] = newInvoices[f].getId();
                invoiceGenerated++;
            }
            LOG.info("The user " + userId + " has been processed." + invoiceGenerated);

        } catch (Throwable e) {
            LOG.error("Exception caught when processing the user " + userId, e);
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // rollback !
            return null; // the user was not processed
        }

        return retValue;
    }

    public BillingProcessDTOEx getDto(Integer processId, Integer languageId) {
        BillingProcessDTOEx retValue = null;

        BillingProcessBL process = new BillingProcessBL(processId);
        retValue = process.getDtoEx(languageId);
        if (retValue != null)
            retValue.toString(); // as a form of touch

        return retValue;
    }

    public BillingProcessConfigurationDTO getConfigurationDto(Integer entityId) throws SessionInternalError {
        BillingProcessConfigurationDTO retValue = null;

        try {
            ConfigurationBL config = new ConfigurationBL(entityId);
            retValue = config.getDTO();
        } catch (Exception e) {
            throw new SessionInternalError(e);
        }

        return retValue;
    }

    public Integer createUpdateConfiguration(Integer executorId, BillingProcessConfigurationDTO dto)
            throws SessionInternalError {
        Integer retValue;

        try {
            LOG.debug("Updating configuration " + dto);
            ConfigurationBL config = new ConfigurationBL();
            retValue = config.createUpdate(executorId, dto);
        } catch (Exception e) {
            throw new SessionInternalError(e);
        }

        return retValue;
    }

    public Integer getLast(Integer entityId) throws SessionInternalError {
        int retValue;

        try {
            BillingProcessBL process = new BillingProcessBL();
            retValue = process.getLast(entityId);
        } catch (Exception e) {
            throw new SessionInternalError(e);
        }

        return retValue > 0 ? new Integer(retValue) : null;
    }

    public BillingProcessDTOEx getReviewDto(Integer entityId, Integer languageId) {
        BillingProcessDTOEx dto = null;
        BillingProcessBL process = new BillingProcessBL();
        dto = process.getReviewDTO(entityId, languageId);
        if (dto != null)
            dto.toString(); // as a touch

        return dto;
    }

    public BillingProcessConfigurationDTO setReviewApproval(Integer executorId, Integer entityId, Boolean flag)
            throws SessionInternalError {
        try {
            LOG.debug("Setting review approval : " + flag);
            ConfigurationBL config = new ConfigurationBL(entityId);
            config.setReviewApproval(executorId, flag.booleanValue());
            return getConfigurationDto(entityId);
        } catch (Exception e) {
            throw new SessionInternalError(e);
        }
    }

    public boolean trigger(Date pToday) throws SessionInternalError {

        if (!running.compareAndSet(false, true)) {
            LOG.warn("Failed to trigger billing process at " + pToday.getTime()
                    + ", another process is already running.");
            return false;
        }

        try {
            Date today = Util.truncateDate(pToday);
            EventLogger eLogger = EventLogger.getInstance();
            BillingProcessBL processBL = new BillingProcessBL();
            GregorianCalendar cal = new GregorianCalendar();

            IBillingProcessSessionBean local = (IBillingProcessSessionBean) Context
                    .getBean(Context.Name.BILLING_PROCESS_SESSION);

            // loop over all the entities
            EntityBL entityBL = new EntityBL();
            Integer entityArray[] = entityBL.getAllIDs();
            LOG.debug("Running trigger. Today = " + today + "[" + today.getTime() + "] entities = "
                    + entityArray.length);

            for (int entityIndex = 0; entityIndex < entityArray.length; entityIndex++) {
                Integer entityId = entityArray[entityIndex];
                LOG.debug("New entity row index " + entityIndex + " of " + entityArray.length);
                LOG.debug("Processing (1) entity " + entityId + " total = " + entityArray.length);

                // now process this entity
                ConfigurationBL configEntity = new ConfigurationBL(entityId);
                BillingProcessConfigurationDTO config = configEntity.getDTO();
                if (!config.getNextRunDate().after(today)) {
                    // there should be a run today 
                    boolean doRun = true;
                    LOG.debug("A process has to be done for entity " + entityId);
                    // check that: the configuration requires a review
                    // AND, there is no partial run already there (failed)
                    if (config.getGenerateReport() == 1
                            && new BillingProcessDAS().isPresent(entityId, 0, config.getNextRunDate()) == null) {

                        // a review had to be done for the run to go ahead
                        boolean reviewPresent = processBL.isReviewPresent(entityId);
                        if (!reviewPresent) { // review wasn't generated
                            LOG.warn("Review is required but not present for " + "entity " + entityId);
                            eLogger.warning(entityId, null, config.getId(), EventLogger.MODULE_BILLING_PROCESS,
                                    EventLogger.BILLING_REVIEW_NOT_GENERATED,
                                    Constants.TABLE_BILLING_PROCESS_CONFIGURATION);

                            generateReview(entityId, config.getNextRunDate(), config.getPeriodUnit().getId(),
                                    config.getPeriodValue());

                            doRun = false;

                        } else if (new Integer(config.getReviewStatus())
                                .equals(Constants.REVIEW_STATUS_GENERATED)) {
                            // the review has to be reviewd yet
                            GregorianCalendar now = new GregorianCalendar();
                            LOG.warn("Review is required but is not approved. Entity " + entityId + " hour is "
                                    + now.get(GregorianCalendar.HOUR_OF_DAY));

                            eLogger.warning(entityId, null, config.getId(), EventLogger.MODULE_BILLING_PROCESS,
                                    EventLogger.BILLING_REVIEW_NOT_APPROVED,
                                    Constants.TABLE_BILLING_PROCESS_CONFIGURATION);

                            try {
                                // only once per day please
                                if (now.get(GregorianCalendar.HOUR_OF_DAY) < 1) {
                                    String params[] = new String[1];
                                    params[0] = entityId.toString();
                                    NotificationBL.sendSapienterEmail(entityId, "process.review_waiting", null,
                                            params);
                                }
                            } catch (Exception e) {
                                LOG.warn("Exception sending an entity email", e);
                            }
                            doRun = false;

                        } else if (new Integer(config.getReviewStatus())
                                .equals(Constants.REVIEW_STATUS_DISAPPROVED)) {
                            // is has been disapproved, let's regenerate
                            LOG.debug("The process should run, but the review has been disapproved");
                            generateReview(entityId, config.getNextRunDate(), config.getPeriodUnit().getId(),
                                    config.getPeriodValue());

                            doRun = false;
                        }
                    }

                    // do the run
                    if (doRun) {
                        local.processEntity(entityId, config.getNextRunDate(), config.getPeriodUnit().getId(),
                                config.getPeriodValue(), false);
                    }

                } else {
                    // no run, may be then a review generation
                    LOG.debug("No run scheduled. Next run on " + config.getNextRunDate().getTime());

                    /*
                     * Review generation
                     */
                    if (config.getGenerateReport() == 1) {
                        cal.setTime(config.getNextRunDate());
                        cal.add(GregorianCalendar.DAY_OF_MONTH, -config.getDaysForReport().intValue());
                        if (!cal.getTime().after(today)) {
                            boolean reviewPresent = processBL.isReviewPresent(entityId);
                            if (reviewPresent
                                    && !Constants.REVIEW_STATUS_DISAPPROVED.equals(config.getReviewStatus())) {
                                // there's already a review there, and it's been
                                // either approved or not yet reviewed
                            } else {
                                LOG.debug("Review disapproved. Regeneratting.");
                                generateReview(entityId, config.getNextRunDate(), config.getPeriodUnit().getId(),
                                        config.getPeriodValue());
                            }
                        }
                    }
                } // else (no run)

                /*
                 * Retries, only if automatic payment is set
                 */
                if (config.getAutoPayment() == 1) {
                    // get the last process
                    Integer[] processToRetry = processBL.getToRetry(entityId);
                    for (Integer aProcessToRetry : processToRetry) {
                        local.doRetry(aProcessToRetry, config.getDaysForRetry(), today);
                    }
                }

            } // for all entities
        } catch (Exception e) {
            throw new SessionInternalError(e);
        } finally {
            running.set(false);
        }
        return true;
    }

    /**
     * @return the id of the invoice generated
     */
    public InvoiceDTO generateInvoice(Integer orderId, Integer invoiceId, Integer languageId)
            throws SessionInternalError {

        try {
            BillingProcessBL process = new BillingProcessBL();
            InvoiceDTO invoice = process.generateInvoice(orderId, invoiceId);
            invoice.touch();

            return invoice;
        } catch (Exception e) {
            throw new SessionInternalError(e);
        }
    }

    public void reviewUsersStatus(Integer entityId, Date today) throws SessionInternalError {
        try {
            AgeingBL age = new AgeingBL();
            age.reviewAll(entityId, today);
        } catch (Exception e) {
            throw new SessionInternalError(e);
        }
    }

    /**
     * Update status of BillingProcessRun in new transaction
     * for accessing updated entity from other thread
     * @param billingProcessId id of billing process for searching ProcessRun
     * @return id of updated ProcessRunDTO
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer updateProcessRunFinished(Integer billingProcessId, Integer processRunStatusId) {
        BillingProcessRunBL runBL = new BillingProcessRunBL();
        runBL.setProcess(billingProcessId);
        runBL.updateFinished(processRunStatusId);
        return runBL.getEntity().getId();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer addProcessRunUser(Integer billingProcessId, Integer userId, Integer status) {
        BillingProcessRunBL runBL = new BillingProcessRunBL();
        runBL.setProcess(billingProcessId);
        return runBL.addProcessRunUser(userId, status).getId();
    }

    /**
     * Returns true if the Billing Process is running.
     */
    public boolean isBillingRunning() {
        return running.get();
    }
}