/* 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.pluggableTask; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import com.sapienter.jbilling.server.order.db.OrderPeriodDTO; import org.apache.log4j.Logger; import com.sapienter.jbilling.common.SessionInternalError; import com.sapienter.jbilling.common.Util; import com.sapienter.jbilling.server.order.db.OrderDTO; import com.sapienter.jbilling.server.order.db.OrderStatusDAS; import com.sapienter.jbilling.server.process.ConfigurationBL; import com.sapienter.jbilling.server.process.PeriodOfTime; import com.sapienter.jbilling.server.util.Constants; import com.sapienter.jbilling.server.util.MapPeriodToCalendar; import com.sapienter.jbilling.server.util.audit.EventLogger; import java.util.ArrayList; public class BasicOrderPeriodTask extends PluggableTask implements OrderPeriodTask { protected Date viewLimit = null; private static final Logger LOG = Logger.getLogger(BasicOrderPeriodTask.class); private List<PeriodOfTime> periods = new ArrayList<PeriodOfTime>(); public BasicOrderPeriodTask() { viewLimit = null; } /** * Calculates the date that the invoice about to be generated is * going to start cover from this order. This IS NOT the invoice * date, since an invoice is composed by (potentially) several orders and * other invoices * @param order * @return */ public Date calculateStart(OrderDTO order) throws TaskException { Date retValue = null; if (order.getOrderPeriod().getId() == Constants.ORDER_PERIOD_ONCE) { // this should be irrelevant, and could be either the order date // or this process date ... return null; } if (order.getNextBillableDay() == null) { // never been processed // If it is open started (with no start date), we assume that // it started when it was created retValue = order.getActiveSince() == null ? order.getCreateDate() : order.getActiveSince(); } else { // the process date means always which day has not been paid // for yet. retValue = order.getNextBillableDay(); } if (retValue == null) { throw new TaskException("Missing some date fields on " + "order " + order.getId()); } // it's important to truncate this date return Util.truncateDate(retValue); } /** * This methods takes and order and calculates the end date that is * going to be covered cosidering the starting date and the dates * of this process. * @param order * @param process * @param startOfBillingPeriod * @return * @throws SessionInternalError */ public Date calculateEnd(OrderDTO order, Date processDate, int maxPeriods, Date startOfBillingPeriod) throws TaskException { if (order.getOrderPeriod().getId() == Constants.ORDER_PERIOD_ONCE) { periods.add(new PeriodOfTime(null, null, 0, 1)); return null; } Date endOfPeriod = null; final Date firstBillableDate = calculateStart(order); GregorianCalendar cal = new GregorianCalendar(); try { // calculate the how far we can see in the future // get the period of time from the process configuration if (viewLimit == null) { viewLimit = getViewLimit(order.getOrderPeriod(), processDate); } cal.setTime(startOfBillingPeriod); LOG.debug("Calculating ebp for order " + order.getId() + " sbp:" + startOfBillingPeriod + " process date: " + processDate + " viewLimit:" + viewLimit); if (!order.getStatusId().equals(Constants.ORDER_STATUS_ACTIVE)) { throw new TaskException("Only active orders should be " + "generating invoice. This " + order.getStatusId()); } if (order.getBillingTypeId().compareTo( Constants.ORDER_BILLING_POST_PAID) == 0 ) { // this will move on time from the start of the billing period // to the closest multiple period that doesn't go beyond the // visibility date while (cal.getTime().compareTo(viewLimit) < 0 && (order.getActiveUntil() == null || cal.getTime().compareTo(order.getActiveUntil()) < 0) && periods.size() < maxPeriods) { Date cycleStarts = cal.getTime(); cal.add(MapPeriodToCalendar.map(order.getOrderPeriod().getUnitId()), order.getOrderPeriod().getValue()); Date cycleEnds = cal.getTime(); if (cycleEnds.after(firstBillableDate) && (cycleEnds.before(viewLimit) || (order.getActiveUntil() != null && order.getActiveUntil().before(viewLimit)))) { // calculate the days for this cycle PeriodOfTime cycle = new PeriodOfTime(cycleStarts, cycleEnds, 0, 0); // not crete this period PeriodOfTime pt = new PeriodOfTime((periods.size() == 0) ? firstBillableDate : endOfPeriod, cal.getTime(), cycle.getDaysInPeriod(), periods.size() + 1); periods.add(pt); endOfPeriod = cal.getTime(); LOG.debug("added period " + pt); } LOG.debug("post paid, now testing:" + cal.getTime() + "(eop) = " + endOfPeriod + " compare " + cal.getTime().compareTo(viewLimit)); } } else if (order.getBillingTypeId().compareTo( Constants.ORDER_BILLING_PRE_PAID) == 0) { /* here the end of the period will be after the start of the billing * process. This means that is NOT taking ALL the periods that are * visible to this process, just the first one after the start of the * process */ // bring the date until it goes over the view limit // (or it reaches the expiration). // This then takes all previous periods that should've been billed // by previous processes Date myStart = firstBillableDate; while (cal.getTime().compareTo(viewLimit) < 0 && (order.getActiveUntil() == null || cal.getTime().compareTo(order.getActiveUntil()) < 0) && periods.size() < maxPeriods) { Date cycleStarts = cal.getTime(); cal.add(MapPeriodToCalendar.map(order.getOrderPeriod().getUnitId()), order.getOrderPeriod().getValue().intValue()); Date cycleEnds = cal.getTime(); if (cal.getTime().after(firstBillableDate)) { // calculate the days for this cycle PeriodOfTime cycle = new PeriodOfTime(cycleStarts, cycleEnds, 0, 0); periods.add(new PeriodOfTime(myStart, cal.getTime(), cycle.getDaysInPeriod(), periods.size() + 1)); myStart = cal.getTime(); } LOG.debug("pre paid, now testing:" + cal.getTime() + "(eop) = " + endOfPeriod + " compare " + cal.getTime().compareTo(viewLimit)); } endOfPeriod = cal.getTime(); } else { throw new TaskException("Order billing type " + order.getBillingTypeId() + " is not supported"); } } catch (Exception e) { throw new TaskException(e); } LOG.debug("Calculated end of period as: " + endOfPeriod); endOfPeriod = verifyEndOfMonthDay(order, endOfPeriod); if (endOfPeriod == null) { throw new TaskException("Error calculating for order " + order.getId()); } else if (order.getActiveUntil() != null && endOfPeriod.after(order.getActiveUntil())) { // make sure this date is not beyond the expiration date endOfPeriod = order.getActiveUntil(); } if (startOfBillingPeriod.compareTo(endOfPeriod) == 0) { // this order should not be in active status periods.clear(); order.setOrderStatus(new OrderStatusDAS().find(Constants.ORDER_STATUS_FINISHED)); new EventLogger().error(order.getBaseUserByUserId().getCompany().getId(), order.getBaseUserByUserId().getId(), order.getId(), EventLogger.MODULE_BILLING_PROCESS, EventLogger.BILLING_PROCESS_WRONG_FLAG_ON, Constants.TABLE_PUCHASE_ORDER); LOG.warn("Calculating the end period for" + " order " + order.getId() + " ends up being the same as the" + " start period. Shouldn't this order be excluded?"); } // make sure the last period actually reflects the last adjustments if (periods.size() > 0) { PeriodOfTime lastOne = periods.get(periods.size() - 1); periods.remove(lastOne); periods.add(new PeriodOfTime(lastOne.getStart(), endOfPeriod, lastOne.getDaysInCycle(), periods.size() + 1)); } LOG.debug("ebp:" + endOfPeriod); return endOfPeriod; } protected Date getViewLimit(OrderPeriodDTO orderPeriod, Date processDate) { Integer periodUnitId = orderPeriod.getPeriodUnit().getId(); Integer periodValue = orderPeriod.getValue(); LOG.debug("Calculating view limit, " + periodValue + " " + orderPeriod.getPeriodUnit().getDescription() + "(s) from " + processDate); Calendar cal = Calendar.getInstance(); cal.setTime(processDate); cal.add(MapPeriodToCalendar.map(periodUnitId), periodValue); return cal.getTime(); } /* * // Last day of the month validation // If the current date is the last day of a month, the next date // might have to as well. */ protected Date verifyEndOfMonthDay(OrderDTO order, Date date) throws TaskException { if (date == null || order == null) return null; GregorianCalendar current = new GregorianCalendar(); // this makes only sense when the order is on monthly periods if (order.getOrderPeriod().getUnitId().equals(Constants.PERIOD_UNIT_MONTH)) { // the current next invoice date has to be the last day of that month, and not a 31 current.setTime(calculateStart(order)); // if the order has a cycle start, then take the day from it // this makes sense for the first invoice. For the rest it should already by it if (order.getCycleStarts() != null) { GregorianCalendar cycleStarts = new GregorianCalendar(); cycleStarts.setTime(order.getCycleStarts()); current.set(Calendar.DAY_OF_MONTH, cycleStarts.get(Calendar.DAY_OF_MONTH)); } if (current.get(Calendar.DAY_OF_MONTH) == current.getActualMaximum(Calendar.DAY_OF_MONTH) && current.get(Calendar.DAY_OF_MONTH) < 31) { // set the end date propsed GregorianCalendar edp = new GregorianCalendar(); edp.setTime(date); // the proposed end date should not be the end of the month if (edp.get(Calendar.DAY_OF_MONTH) != edp.getActualMaximum(Calendar.DAY_OF_MONTH)) { // set the first invoicabe day GregorianCalendar firstDate = new GregorianCalendar(); firstDate.setTime(order.getActiveSince() == null ? order.getCreateDate() : order.getActiveSince()); if (firstDate.get(Calendar.DAY_OF_MONTH) > edp.get(Calendar.DAY_OF_MONTH)) { LOG.debug("Order " + order.getId() + ".Adjusting next invoice date " + "because end of the month from " + edp.get(Calendar.DAY_OF_MONTH) + " to " + firstDate.get(Calendar.DAY_OF_MONTH)); edp.set(Calendar.DAY_OF_MONTH, firstDate.get(Calendar.DAY_OF_MONTH)); return edp.getTime(); } else { // the first date of invoice has to be grater than the day being proposed, otherwise // there isn't anything to fix (the fix is to increas the edp by a few days) return date; } } else { // if the proposed end date is the end of the month, it can't be corrected, since // the correction means adding days. return date; } } else { // if the last next billing date is the 31, adding a month can't be problematic // if the last next billing date is not the last day of the month, it can't come from // a higher end date return date; } } else { return date; } } public List<PeriodOfTime> getPeriods() { return periods; } }