/* 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.Calendar; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.GregorianCalendar; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.List; import com.sapienter.jbilling.server.process.event.InvoicesGeneratedEvent; import org.apache.log4j.Logger; import javax.sql.rowset.CachedRowSet; 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.NewInvoiceDTO; import com.sapienter.jbilling.server.invoice.db.InvoiceDAS; import com.sapienter.jbilling.server.invoice.db.InvoiceDTO; import com.sapienter.jbilling.server.invoice.db.InvoiceStatusDAS; import com.sapienter.jbilling.server.item.CurrencyBL; import com.sapienter.jbilling.server.list.ResultList; import com.sapienter.jbilling.server.order.OrderBL; import com.sapienter.jbilling.server.order.TimePeriod; import com.sapienter.jbilling.server.order.db.OrderDAS; import com.sapienter.jbilling.server.order.db.OrderDTO; import com.sapienter.jbilling.server.order.db.OrderProcessDAS; import com.sapienter.jbilling.server.order.db.OrderProcessDTO; import com.sapienter.jbilling.server.order.event.OrderAddedOnInvoiceEvent; import com.sapienter.jbilling.server.order.event.OrderToInvoiceEvent; import com.sapienter.jbilling.server.payment.IPaymentSessionBean; import com.sapienter.jbilling.server.payment.PaymentBL; import com.sapienter.jbilling.server.payment.db.PaymentMethodDAS; import com.sapienter.jbilling.server.payment.db.PaymentMethodDTO; import com.sapienter.jbilling.server.pluggableTask.InvoiceCompositionTask; import com.sapienter.jbilling.server.pluggableTask.InvoiceFilterTask; import com.sapienter.jbilling.server.pluggableTask.OrderFilterTask; import com.sapienter.jbilling.server.pluggableTask.OrderPeriodTask; import com.sapienter.jbilling.server.pluggableTask.TaskException; import com.sapienter.jbilling.server.pluggableTask.admin.PluggableTaskException; import com.sapienter.jbilling.server.pluggableTask.admin.PluggableTaskManager; import com.sapienter.jbilling.server.process.db.*; import com.sapienter.jbilling.server.system.event.EventManager; import com.sapienter.jbilling.server.user.UserBL; import com.sapienter.jbilling.server.user.db.CustomerDTO; import com.sapienter.jbilling.server.user.db.UserDTO; import com.sapienter.jbilling.server.util.Constants; import com.sapienter.jbilling.server.util.MapPeriodToCalendar; import com.sapienter.jbilling.server.util.PreferenceBL; import com.sapienter.jbilling.server.util.audit.EventLogger; import com.sapienter.jbilling.server.util.db.CurrencyDAS; import com.sapienter.jbilling.server.util.db.CurrencyDTO; import com.sapienter.jbilling.server.util.Context; import java.util.ArrayList; import org.springframework.dao.EmptyResultDataAccessException; public class BillingProcessBL extends ResultList implements ProcessSQL { private BillingProcessDAS billingProcessDas = null; private BillingProcessDTO billingProcess = null; private ProcessRunDAS processRunHome = null; private ProcessRunDTO processRun = null; private static final Logger LOG = Logger.getLogger(BillingProcessBL.class); private EventLogger eLogger = null; public BillingProcessBL(Integer billingProcessId) { init(); set(billingProcessId); } public BillingProcessBL() { init(); } public BillingProcessBL(BillingProcessDTO row) { init(); billingProcess = row; } private void init() { eLogger = EventLogger.getInstance(); billingProcessDas = new BillingProcessDAS(); // now create the run info row processRunHome = new ProcessRunDAS(); } public BillingProcessDTO getEntity() { return billingProcess; } public ProcessRunDTO getProcessRun() { return processRun; } public BillingProcessDAS getHome() { return billingProcessDas; } public void set(Integer id) { billingProcess = billingProcessDas.find(id); } public void set(BillingProcessDTO pEntity) { billingProcess = pEntity; } public Integer findOrCreate(BillingProcessDTO dto) { billingProcess = billingProcessDas.isPresent(dto.getEntity().getId(), dto.getIsReview(), dto.getBillingDate()); if (billingProcess == null) { create(dto); } return billingProcess.getId(); } public Integer create(BillingProcessDTO dto) { // create the record billingProcess = billingProcessDas.create(dto.getEntity(), dto.getBillingDate(), dto.getPeriodUnit().getId(), dto.getPeriodValue(), dto.getRetriesToDo()); billingProcess.setIsReview(dto.getIsReview()); processRun = processRunHome.create( billingProcess, dto.getBillingDate(), 0, new ProcessRunStatusDAS().find(Constants.PROCESS_RUN_STATUS_RINNING)); if (dto.getIsReview() == 1) { ConfigurationBL config = new ConfigurationBL(dto.getEntity().getId()); config.getEntity().setReviewStatus(Constants.REVIEW_STATUS_GENERATED); } return billingProcess.getId(); } /** * 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; } public InvoiceDTO[] generateInvoice( BillingProcessDTO process, UserDTO user, boolean isReview, boolean onlyRecurring) throws SessionInternalError { Integer userId = user.getUserId(); Integer entityId = user.getEntity().getId(); // get the configuration boolean useProcessDateForInvoice = true; int maximumPeriods = 1; boolean paymentApplication = false; try { ConfigurationBL config = new ConfigurationBL(process.getEntity().getId()); useProcessDateForInvoice = config.getEntity().getInvoiceDateProcess() == 1; maximumPeriods = config.getEntity().getMaximumPeriods(); paymentApplication = config.getEntity().getAutoPaymentApplication() == 1; } catch (Exception e) { // swallow exception } // this contains the generated invoices, one per due date // found in the applicable purchase orders. // The key is the object TimePeriod Hashtable<TimePeriod, NewInvoiceDTO> newInvoices = new Hashtable<TimePeriod, NewInvoiceDTO>(); InvoiceDTO[] retValue = null; LOG.debug("In generateInvoice for user " + userId + " process date:" + process.getBillingDate()); /* * Go through the orders first * This method will recursively call itself to find sub-accounts in any * level */ boolean includedOrders = processOrdersForUser(user, entityId, process, isReview, onlyRecurring, useProcessDateForInvoice, maximumPeriods, newInvoices); if (!includedOrders || newInvoices.size() == 0) { // check if invoices without orders are allowed PreferenceBL preferenceBL = new PreferenceBL(); try { preferenceBL.set(entityId, Constants.PREFERENCE_ALLOW_INVOICES_WITHOUT_ORDERS); } catch (EmptyResultDataAccessException fe) { // use default } if (preferenceBL.getInt() == 0) { LOG.debug("No applicable orders. No invoice generated (skipping invoices)."); return null; } } if (!isReview) { for (Map.Entry<TimePeriod, NewInvoiceDTO> newInvoiceEntry : newInvoices.entrySet()) { // process events before orders added to invoice processOrderToInvoiceEvents(newInvoiceEntry.getValue(), entityId); } } /* * Include those invoices that should've been paid * (or have negative balance, as credits) */ LOG.debug("Considering overdue invoices"); // find the invoice home interface InvoiceDAS invoiceDas = new InvoiceDAS(); // any of the new invoices being created could hold the overdue invoices NewInvoiceDTO holder = newInvoices.isEmpty() ? null : (NewInvoiceDTO) newInvoices.elements().nextElement(); Collection dueInvoices = invoiceDas.findWithBalanceByUser(user); LOG.debug("Processing invoices for user " + user.getUserId()); // go through each of them, and update the DTO if it applies for (Iterator it = dueInvoices.iterator(); it.hasNext();) { InvoiceDTO invoice = (InvoiceDTO) it.next(); LOG.debug("Processing invoice " + invoice.getId()); // apply any invoice processing filter pluggable task try { PluggableTaskManager taskManager = new PluggableTaskManager(entityId, Constants.PLUGGABLE_TASK_INVOICE_FILTER); InvoiceFilterTask task = (InvoiceFilterTask) taskManager.getNextClass(); boolean isProcessable = true; while (task != null) { isProcessable = task.isApplicable(invoice, process); if (!isProcessable) { break; // no need to keep doing more tests } task = (InvoiceFilterTask) taskManager.getNextClass(); } // include this invoice only if it complies with all the rules if (isProcessable) { // check for an invoice if (holder == null) { // Since there are no new invoices (therefore no orders), // don't let invoices with positive balances generate // an invoice. if (BigDecimal.ZERO.compareTo(invoice.getBalance()) < 0) { continue; } // no invoice/s yet (no applicable orders), so create one holder = new NewInvoiceDTO(); holder.setDate(process.getBillingDate()); holder.setIsReview(isReview ? new Integer(1) : new Integer(0)); holder.setCarriedBalance(BigDecimal.ZERO); holder.setInvoiceStatus(new InvoiceStatusDAS().find(Constants.INVOICE_STATUS_UNPAID)); // need to set a due date, so use the order default OrderBL orderBl = new OrderBL(); OrderDTO order = new OrderDTO(); order.setBaseUserByUserId(user); orderBl.set(order); TimePeriod dueDatePeriod = orderBl.getDueDate(); holder.setDueDatePeriod(dueDatePeriod); newInvoices.put(dueDatePeriod, holder); } InvoiceBL ibl = new InvoiceBL(invoice); holder.addInvoice(ibl.getDTO()); // for those invoices wiht only overdue invoices, the // currency has to be initialized if (holder.getCurrency() == null) { holder.setCurrency(invoice.getCurrency()); } else if (holder.getCurrency().getId() != invoice.getCurrency().getId()) { throw new SessionInternalError("An invoice with different " + "currency is not supported. " + "Currency = " + holder.getCurrency().getId() + "invoice = " + invoice.getId()); } // update the amount of the new invoice that is due to // previously unpaid overdue invoices // carry the remaining balance, plus the previously carried balance to the new invoice BigDecimal balance = (invoice.getBalance() == null) ? BigDecimal.ZERO : invoice.getBalance(); BigDecimal carried = balance.add(holder.getCarriedBalance()); holder.setCarriedBalance(carried); } LOG.debug("invoice " + invoice.getId() + " result " + isProcessable); } catch (PluggableTaskException e) { LOG.fatal("Problems handling task invoice filter.", e); throw new SessionInternalError("Problems handling task invoice filter."); } catch (TaskException e) { LOG.fatal("Problems excecuting task invoice filter.", e); throw new SessionInternalError("Problems executing task invoice filter."); } } if (newInvoices.size() == 0) { // no orders or invoices for this invoice LOG.debug("No applicable orders or invoices. No invoice generated (skipping invoices)."); return null; } try { retValue = new InvoiceDTO[newInvoices.size()]; int index = 0; for (NewInvoiceDTO invoice : newInvoices.values()) { /* * Apply invoice composition tasks to the new invoices object */ composeInvoice(entityId, user.getUserId(), invoice); if (!isReview) { // process events after orders added to invoice processOrderAddedOnInvoiceEvents(invoice, entityId); for (InvoiceDTO oldInvoice : invoice.getInvoices()) { // since this invoice is being delegated, mark it as being carried forward // so that it is not re-processed later. do not clear the old balance! oldInvoice.setInvoiceStatus(new InvoiceStatusDAS().find(Constants.INVOICE_STATUS_UNPAID_AND_CARRIED)); } } /* * apply this object to the DB, generating the actual rows */ // only if the invoice generated actually has some lines in it if (invoice.areLinesGeneratedEmpty()) { LOG.warn("User " + user.getUserId() + " had orders or invoices but" + " the invoice composition task didn't generate any lines."); continue; } // If this is a web services API call, the billing // process id is 0. Don't link to the billing process // object for API calls. retValue[index] = generateDBInvoice(user.getUserId(), invoice, (process.getId() != 0 ? process : null), Constants.ORDER_PROCESS_ORIGIN_PROCESS); // try to get this new invioce paid by previously unlinked // payments if (paymentApplication && !isReview) { PaymentBL pBL = new PaymentBL(); pBL.automaticPaymentApplication(retValue[index]); } index++; } } catch (PluggableTaskException e1) { LOG.error("Error handling pluggable tasks when composing an invoice"); throw new SessionInternalError(e1); } catch (TaskException e1) { LOG.error("Task exception when composing an invoice"); throw new SessionInternalError(e1); } catch (Exception e1) { LOG.error("Error, probably linking payments", e1); throw new SessionInternalError(e1); } InvoicesGeneratedEvent generatedEvent = new InvoicesGeneratedEvent(entityId, process.getId()); generatedEvent.addInvoices(retValue); EventManager.process(generatedEvent); return retValue; } private boolean processOrdersForUser(UserDTO user, Integer entityId,BillingProcessDTO process, boolean isReview, boolean onlyRecurring, boolean useProcessDateForInvoice, int maximumPeriods, Hashtable<TimePeriod, NewInvoiceDTO> newInvoices) { boolean includedOrders = false; Integer userId = user.getUserId(); LOG.debug("Processing orders for user " + userId); // initialize the subaccounts iterator if this user is a parent Iterator subAccountsIt = null; if (user.getCustomer().getIsParent() != null && user.getCustomer().getIsParent().intValue() == 1) { UserBL parent = new UserBL(userId); subAccountsIt = parent.getEntity().getCustomer().getChildren(). iterator(); } // get the orders that might be processable for this user OrderDAS orderDas = new OrderDAS(); Collection<OrderDTO> orders = orderDas.findByUser_Status(userId, Constants.ORDER_STATUS_ACTIVE); // go through each of them, and update the DTO if it applies for (OrderDTO order : orders) { LOG.debug("Processing order :" + order.getId()); // apply any order processing filter pluggable task try { PluggableTaskManager taskManager = new PluggableTaskManager( entityId, Constants.PLUGGABLE_TASK_ORDER_FILTER); OrderFilterTask task = (OrderFilterTask) taskManager.getNextClass(); boolean isProcessable = true; while (task != null) { isProcessable = task.isApplicable(order, process); if (!isProcessable) { break; // no need to keep doing more tests } task = (OrderFilterTask) taskManager.getNextClass(); } // include this order only if it complies with all the // rules if (isProcessable) { LOG.debug("Order processable"); if (onlyRecurring) { if (order.getOrderPeriod().getId() != Constants.ORDER_PERIOD_ONCE) { includedOrders = true; LOG.debug("It is not one-timer. " + "Generating invoice"); } } else { includedOrders = true; } /* * now find if there is already an invoice being * generated for the given due date period */ // find the due date that applies to this order OrderBL orderBl = new OrderBL(); orderBl.set(order); TimePeriod dueDatePeriod = orderBl.getDueDate(); // look it up in the hashtable NewInvoiceDTO thisInvoice = (NewInvoiceDTO) newInvoices.get(dueDatePeriod); if (thisInvoice == null) { LOG.debug("Adding new invoice for period " + dueDatePeriod + " process date:" + process.getBillingDate()); // we need a new one with this period // define the invoice date thisInvoice = new NewInvoiceDTO(); if (useProcessDateForInvoice) { thisInvoice.setDate(process.getBillingDate()); } else { thisInvoice.setDate(orderBl.getInvoicingDate(), order.getOrderPeriod().getId() != Constants.ORDER_PERIOD_ONCE); } thisInvoice.setIsReview(isReview ? new Integer(1) : new Integer(0)); thisInvoice.setCarriedBalance(BigDecimal.ZERO); thisInvoice.setDueDatePeriod(dueDatePeriod); } else { LOG.debug("invoice found for period " + dueDatePeriod); if (!useProcessDateForInvoice) { thisInvoice.setDate(orderBl.getInvoicingDate(), order.getOrderPeriod().getId() != Constants.ORDER_PERIOD_ONCE); } } /* * The order periods plug-in might not add any period. This should not happen * but if it does, the invoice should not be included */ if (addOrderToInvoice(entityId, order, thisInvoice, process.getBillingDate(), maximumPeriods)) { // add or replace newInvoices.put(dueDatePeriod, thisInvoice); } LOG.debug("After putting period there are " + newInvoices.size() + " periods."); // here it would be easy to update this order // to_process and // next_billable_time. I can't do that because these // fields // will be read by the following tasks, and they // will asume // they are not modified. } } catch (PluggableTaskException e) { LOG.fatal("Problems handling order filter task.", e); throw new SessionInternalError( "Problems handling order filter task."); } catch (TaskException e) { LOG.fatal("Problems excecuting order filter task.", e); throw new SessionInternalError( "Problems executing order filter task."); } } // for - all the orders for this user // see if there is any subaccounts to include in this invoice while (subAccountsIt != null) { // until there are no more subaccounts (subAccountsIt != null) { CustomerDTO customer = null; while (subAccountsIt.hasNext()) { customer = (CustomerDTO) subAccountsIt.next(); if (customer.getInvoiceChild() == null || customer.getInvoiceChild().intValue() == 0) { break; } else { LOG.debug("Subaccount not included in parent's invoice " + customer.getId()); customer = null; } } if (customer != null) { userId = customer.getBaseUser().getUserId(); // if the child does not have any orders to invoice, this should // not affect the current value of includedOrders if (processOrdersForUser(customer.getBaseUser(), entityId, process, isReview, onlyRecurring, useProcessDateForInvoice, maximumPeriods, newInvoices)) { // if ANY child has orders to invoice, it is enough for includedOrders to be true includedOrders = true; } LOG.debug("Now processing subaccount " + userId); } else { subAccountsIt = null; LOG.debug("No more subaccounts to process"); } } return includedOrders; } private InvoiceDTO generateDBInvoice(Integer userId, NewInvoiceDTO newInvoice, BillingProcessDTO process, Integer origin) throws SessionInternalError { // The invoice row is created first // all that fits in the DTO goes there newInvoice.calculateTotal(); if (newInvoice.getCarriedBalance() != null) { newInvoice.setBalance(newInvoice.getTotal().subtract(newInvoice.getCarriedBalance())); } else { newInvoice.setBalance(newInvoice.getTotal()); } newInvoice.setInProcessPayment(new Integer(1)); InvoiceBL invoiceBL = new InvoiceBL(); try { invoiceBL.create(userId, newInvoice, process); invoiceBL.createLines(newInvoice); } catch (Exception e) { LOG.fatal("CreateException creating invoice record", e); throw new SessionInternalError("Couldn't create the invoice record"); } createOrderProcess(newInvoice, invoiceBL.getEntity(), process, origin); return invoiceBL.getEntity(); } private void createOrderProcess(NewInvoiceDTO newInvoice, InvoiceDTO invoice, BillingProcessDTO process, Integer origin) throws SessionInternalError { LOG.debug("Generating order process records..."); // update the orders involved, now that their old data is not needed // anymore for (int f = 0; f < newInvoice.getOrders().size(); f++) { OrderDTO order = (OrderDTO) newInvoice.getOrders().get(f); LOG.debug(" ... order " + order.getId()); // this will help later List<PeriodOfTime> periodsList = newInvoice.getPeriods().get(f); Date startOfBillingPeriod = (Date) periodsList.get(0).getStart(); Date endOfBillingPeriod = periodsList.get(periodsList.size() - 1).getEnd(); Integer periods = (Integer) newInvoice.getPeriods().get(f).size(); // We don't update orders if this is just a review if (newInvoice.getIsReview().intValue() == 0) { // update the to_process if applicable updateStatusFinished(order, startOfBillingPeriod, endOfBillingPeriod); // update this order process field updateNextBillableDay(order, endOfBillingPeriod); } // create the period and update the order-invoice relationship try { OrderProcessDAS das = new OrderProcessDAS(); OrderProcessDTO orderProcess = new OrderProcessDTO(); orderProcess.setPeriodStart(startOfBillingPeriod); orderProcess.setPeriodEnd(endOfBillingPeriod); orderProcess.setIsReview(newInvoice.getIsReview()); orderProcess.setPurchaseOrder(order); InvoiceDAS invDas = new InvoiceDAS(); orderProcess.setInvoice(invDas.find(invoice.getId())); BillingProcessDAS proDas = new BillingProcessDAS(); orderProcess.setBillingProcess(process != null ? proDas.find(process.getId()) : null); orderProcess.setPeriodsIncluded(periods); orderProcess.setOrigin(origin); orderProcess = das.save(orderProcess); LOG.debug("created order process id " + orderProcess.getId() + " for order " + order.getId()); } catch (Exception e) { throw new SessionInternalError(e); } } } private void composeInvoice(Integer entityId, Integer userId, NewInvoiceDTO newInvoice) throws PluggableTaskException, TaskException, SessionInternalError { newInvoice.setEntityId(entityId); PluggableTaskManager taskManager = new PluggableTaskManager(entityId, Constants.PLUGGABLE_TASK_INVOICE_COMPOSITION); InvoiceCompositionTask task = (InvoiceCompositionTask) taskManager.getNextClass(); while (task != null) { task.apply(newInvoice, userId); task = (InvoiceCompositionTask) taskManager.getNextClass(); } String validationMessage = newInvoice.validate(); if (validationMessage != null) { LOG.error( "Composing invoice for entity " + entityId + " invalid new invoice object: " + validationMessage); throw new SessionInternalError( "NewInvoiceDTO:" + validationMessage); } } 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; } static void updateStatusFinished(OrderDTO order, Date startOfBillingPeriod, Date endOfBillingPeriod) throws SessionInternalError { // all one timers are done if (order.getOrderPeriod().getId() == Constants.ORDER_PERIOD_ONCE) { OrderBL orderBL = new OrderBL(order); orderBL.setStatus(null, Constants.ORDER_STATUS_FINISHED); } else { // recursive orders get more complicated // except those that are immortal :) if (order.getActiveUntil() == null) { return; } // see until when the incoming process will cover // compare if this is after the order exipres Logger log = Logger.getLogger(BillingProcessBL.class); log.debug("order " + order.getId() + "end of bp " + endOfBillingPeriod + " active until " + order.getActiveUntil()); if (endOfBillingPeriod.compareTo(Util.truncateDate(order.getActiveUntil())) >= 0) { OrderBL orderBL = new OrderBL(order); orderBL.setStatus(null, Constants.ORDER_STATUS_FINISHED); } } } static public Date getEndOfProcessPeriod(BillingProcessDTO process) throws SessionInternalError { GregorianCalendar cal = new GregorianCalendar(); cal.setTime(process.getBillingDate()); cal.add(MapPeriodToCalendar.map(process.getPeriodUnit().getId()), process.getPeriodValue()); return cal.getTime(); } static public void updateNextBillableDay(OrderDTO order, Date end) throws SessionInternalError { // if this order won't be process ever again, the // it shouldn't have a next billable day if (order.getStatusId().equals(Constants.ORDER_STATUS_FINISHED)) { order.setNextBillableDay(null); } else { order.setNextBillableDay(end); } } public BillingProcessDTOEx getDtoEx(Integer language) { BillingProcessDTOEx retValue = new BillingProcessDTOEx(); retValue.setBillingDate(billingProcess.getBillingDate()); retValue.setEntity(billingProcess.getEntity()); retValue.setId(billingProcess.getId()); retValue.setPeriodUnit(billingProcess.getPeriodUnit()); retValue.setPeriodValue(billingProcess.getPeriodValue()); retValue.setIsReview(billingProcess.getIsReview()); // now add the runs and grand total BillingProcessRunDTOEx grandTotal = new BillingProcessRunDTOEx(); int totalInvoices = 0; int runsCounter = 0; List<BillingProcessRunDTOEx> runs = new ArrayList<BillingProcessRunDTOEx>(); // go throuhg every run for (Iterator it = billingProcess.getProcessRuns().iterator(); it.hasNext();) { ProcessRunDTO run = (ProcessRunDTO) it.next(); BillingProcessRunBL runBL = new BillingProcessRunBL(run); BillingProcessRunDTOEx runDto = runBL.getDTO(language); runs.add(runDto); runsCounter++; // add statistic for InProgress run proccess in DTO if (run.getPaymentFinished() == null) { addRuntimeStatistic(run.getBillingProcess().getId(), language, runDto); } LOG.debug("Run:" + run.getId() + " has " + run.getProcessRunTotals().size() + " total records"); // go over the totals, since there's one per currency for (Iterator it2 = runDto.getTotals().iterator(); it2.hasNext();) { // the total to process BillingProcessRunTotalDTOEx totalDto = (BillingProcessRunTotalDTOEx) it2.next(); BillingProcessRunTotalDTOEx sum = getTotal(totalDto.getCurrency(), grandTotal.getTotals()); BigDecimal totalTmp = totalDto.getTotalInvoiced().add(sum.getTotalInvoiced()); sum.setTotalInvoiced(totalTmp); totalTmp = totalDto.getTotalPaid().add(sum.getTotalPaid()); sum.setTotalPaid(totalTmp); // can't add up the not paid, because many runs will try to // get the same invoices paid, so the not paid field gets // duplicated amounts. totalTmp = sum.getTotalInvoiced().subtract(sum.getTotalPaid()); sum.setTotalNotPaid(totalTmp); // make sure this total has the currency name initialized if (sum.getCurrencyName() == null) { CurrencyBL currency = new CurrencyBL(sum.getCurrency().getId()); sum.setCurrencyName(currency.getEntity().getDescription( language)); } // add the payment method totals for (Enumeration en = totalDto.getPmTotals().keys(); en.hasMoreElements();) { String method = (String) en.nextElement(); BigDecimal methodTotal = new BigDecimal(totalDto.getPmTotals().get(method).toString()); if (sum.getPmTotals().containsKey(method)) { if (sum.getPmTotals().get(method) != null) { methodTotal = methodTotal.add(new BigDecimal(((Float) sum.getPmTotals(). get(method)).toString())); } } sum.getPmTotals().put(method, new Float(methodTotal.floatValue())); } LOG.debug("Added total to run dto. PMs in total:" + sum.getPmTotals().size() + " now grandTotal totals:" + grandTotal.getTotals().size()); } totalInvoices += runDto.getInvoicesGenerated(); } grandTotal.setInvoicesGenerated(new Integer(totalInvoices)); retValue.setRetries(new Integer(runsCounter)); retValue.setRuns(runs); retValue.setGrandTotal(grandTotal); retValue.setBillingDateEnd(getEndOfProcessPeriod(billingProcess)); retValue.setOrdersProcessed(new Integer(billingProcess.getOrderProcesses().size())); return retValue; } private void addRuntimeStatistic(Integer billingProcessId, Integer language, BillingProcessRunDTOEx runDto) { for (Iterator iter = new BillingProcessDAS().getCountAndSum(billingProcessId); iter.hasNext();) { Object[] row = (Object[]) iter.next(); BillingProcessRunTotalDTOEx totalRowDto = new BillingProcessRunTotalDTOEx(); totalRowDto.setProcessRun(runDto); totalRowDto.setCurrency(new CurrencyDAS().find((Integer) row[2])); totalRowDto.setCurrencyName(totalRowDto.getCurrency().getDescription(language)); totalRowDto.setId(-1); totalRowDto.setTotalInvoiced((BigDecimal) row[1]); totalRowDto.setTotalNotPaid(BigDecimal.ZERO); totalRowDto.setTotalPaid(BigDecimal.ZERO); // now go over the totals by payment method Hashtable totals = new Hashtable(); for (Iterator itt = new BillingProcessDAS().getSuccessfulProcessCurrencyMethodAndSum(billingProcessId); itt.hasNext();) { Object[] payedRow = (Object[]) itt.next(); if (payedRow[0].equals(totalRowDto.getCurrency().getId())) { PaymentMethodDTO paymentMethod = new PaymentMethodDAS().find((Integer) payedRow[1]); BigDecimal payed = (BigDecimal) payedRow[2]; totals.put(paymentMethod.getDescription(language), payed); totalRowDto.setTotalPaid(totalRowDto.getTotalPaid().add(payed)); } } totalRowDto.setPmTotals(totals); for (Iterator itt = new BillingProcessDAS().getFailedProcessCurrencyAndSum(billingProcessId); itt.hasNext();) { Object[] unpayedRow = (Object[]) itt.next(); if (unpayedRow[0].equals(totalRowDto.getCurrency().getId())) { totalRowDto.setTotalNotPaid(totalRowDto.getTotalNotPaid().add((BigDecimal) unpayedRow[1])); } } runDto.setInvoicesGenerated(runDto.getInvoicesGenerated() + ((Long) row[0]).intValue()); runDto.getTotals().add(totalRowDto); } } public CachedRowSet getList(Integer entityId) throws SQLException, Exception { prepareStatement(ProcessSQL.generalList); cachedResults.setInt(1, entityId.intValue()); execute(); conn.close(); return cachedResults; } public int getLast(Integer entityId) throws SQLException, Exception { int retValue = -1; prepareStatement(ProcessSQL.lastId); cachedResults.setInt(1, entityId.intValue()); execute(); conn.close(); if (cachedResults.next()) { retValue = cachedResults.getInt(1); } return retValue; } public Integer[] getToRetry(Integer entityId) throws SQLException, Exception { List list = new ArrayList(); prepareStatement(ProcessSQL.findToRetry); cachedResults.setInt(1, entityId.intValue()); execute(); conn.close(); while (cachedResults.next()) { list.add(new Integer(cachedResults.getInt(1))); } Integer retValue[] = new Integer[list.size()]; list.toArray(retValue); return retValue; } /** * Tries to get paid the invoice of the parameter. * The processs Id and runId are only to update the run totals. * Only one of them is required. The runId should be passed if * this is a retry, otherwise the processId. * @param processId * @param runId * @param invoiceId * @throws SessionInternalError */ public void generatePayment(Integer processId, Integer runId, Integer invoiceId) throws SessionInternalError { try { InvoiceBL invoiceBL = new InvoiceBL(invoiceId); InvoiceDTO newInvoice = invoiceBL.getEntity(); IPaymentSessionBean paymentSess = (IPaymentSessionBean) Context.getBean(Context.Name.PAYMENT_SESSION); paymentSess.generatePayment(newInvoice); } catch (Exception e) { throw new SessionInternalError(e); } } public BillingProcessRunTotalDTOEx getTotal(CurrencyDTO currency, List totals) { BillingProcessRunTotalDTOEx retValue = null; for (int f = 0; f < totals.size(); f++) { BillingProcessRunTotalDTOEx total = (BillingProcessRunTotalDTOEx) totals.get(f); if (total.getCurrency().equals(currency)) { retValue = total; break; } } // it is looking for a total that doesn't exist if (retValue == null) { CurrencyDAS curDas = new CurrencyDAS(); CurrencyDTO curDto = curDas.find(currency.getId()); retValue = new BillingProcessRunTotalDTOEx(null, curDto, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO); totals.add(retValue); } return retValue; } public BillingProcessDTOEx getReviewDTO(Integer entityId, Integer languageId) { billingProcess = billingProcessDas.findReview(entityId); if (billingProcess == null) { System.out.println("Don't found the billingProcess"); return null; } else { System.out.println("found the billingProcess"); } return getDtoEx(languageId); } public boolean isReviewPresent(Integer entityId) { return billingProcessDas.findReview(entityId) != null; } 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); } @SuppressWarnings("unchecked") private void processOrderToInvoiceEvents(NewInvoiceDTO newInvoice, Integer entityId) { List<OrderDTO> orders = newInvoice.getOrders(); List<List<PeriodOfTime>> periods = newInvoice.getPeriods(); for (int i = 0; i < orders.size(); i++) { OrderDTO order = orders.get(i); Integer userId = findUserId(order); for (PeriodOfTime period : periods.get(i)) { OrderToInvoiceEvent newEvent = new OrderToInvoiceEvent(entityId, userId, order); newEvent.setStart(period.getStart()); newEvent.setEnd(period.getEnd()); EventManager.process(newEvent); } } } @SuppressWarnings("unchecked") private void processOrderAddedOnInvoiceEvents(NewInvoiceDTO newInvoice, Integer entityId) { List<OrderDTO> orders = newInvoice.getOrders(); List<List<PeriodOfTime>> periods = newInvoice.getPeriods(); for (int i = 0; i < orders.size(); i++) { OrderDTO order = orders.get(i); Integer userId = findUserId(order); for (PeriodOfTime period : periods.get(i)) { LOG.info("Number of orders in map: " + newInvoice.getOrderTotalContributions().size()); LOG.info("Map: " + newInvoice.getOrderTotalContributions()); OrderAddedOnInvoiceEvent newEvent = new OrderAddedOnInvoiceEvent(entityId, userId, order, newInvoice.getOrderTotalContributions().get( order.getId())); newEvent.setStart(period.getStart()); newEvent.setEnd(period.getEnd()); EventManager.process(newEvent); } } } private Integer findUserId(OrderDTO order) { UserDTO user = order.getUser(); // while this user has a parent and the flag is off, keep looking while(user.getCustomer().getParent() != null && (user.getCustomer().getInvoiceChild() == null || user.getCustomer().getInvoiceChild() == 0)) { // go up one level LOG.debug("finding parent for invoicing. Now " + user.getUserId()); user = user.getCustomer().getParent().getBaseUser(); } return user.getUserId(); } /** * Convert a given BillingProcessDTO into a BillingProcessWS web-service object. * * @param dto dto to convert * @return converted web-service object */ public static BillingProcessWS getWS(BillingProcessDTO dto) { return dto != null ? new BillingProcessWS(dto) : null; } /** * Convert a given BillingProcessDTOEx into a BillingProcessWS web-service object. * * @param ex extended DTO to convert * @return converted web-service object */ public static BillingProcessWS getWS(BillingProcessDTOEx ex) { return ex != null ? new BillingProcessWS(ex) : null; } }