/*
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.order;
import com.sapienter.jbilling.server.order.db.OrderDTO;
import com.sapienter.jbilling.server.order.db.OrderLineDTO;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* OrderHelper
*
* Swap item
* Swap excess
* Merge lines (combine prices & quantities)
* Flatten (merges all lines where the item id is equal)
* Purge item (remove all lines of item)
* Copy
*
*
* @author Brian Cowdery
* @since 28/06/11
*/
public class OrderHelper {
/**
* Replaces all existing order lines with a different item.
*
* @param order order containing lines to swap
* @param oldItemId old item id (to be swapped out)
* @param newItemId new item id
* @return order with swapped lines
*/
public static OrderDTO swap(OrderDTO order, Integer oldItemId, Integer newItemId) {
throw new UnsupportedOperationException("Swap not yet implemented.");
}
/**
* Replaces a quantity of existing order lines with a different item.
*
* If the order contains less than the desires swap quantity then the entire existing order
* line will be removed an equivalent line will be added using the new item.
*
* If the order contains more than the desired swap quantity then the desired swap quantity
* will be subtracted from the existing line and a new line will be added with the new item.
*
* @param order order containing lines to swap
* @param oldItemId old item id (to be swapped out)
* @param newItemId new item id
* @param quantity quantity to swap if available
* @return order with swapped lines
*/
public static OrderDTO swap(OrderDTO order, Integer oldItemId, Integer newItemId, BigDecimal quantity) {
throw new UnsupportedOperationException("Swap not yet implemented.");
}
/**
* Replaces excess of an existing order lines with a different item when the total line quantity
* exceeds the given quantity.
*
* @param order order containing lines to swap
* @param oldItemId old item id (to be swapped out)
* @param newItemId new item id
* @param quantity target quantity, items will be swapped if the existing order quantity exceeds this value.
* @return order with swapped lines
*/
public static OrderDTO swapExcess(OrderDTO order, Integer oldItemId, Integer newItemId, BigDecimal quantity) {
throw new UnsupportedOperationException("Swap excess not yet implemented.");
}
/**
* Replaces excess of an existing order lines with a new line at a set price when the total line quantity
* exceeds the given quantity.
*
* This operates the same as {@link #swapExcess(OrderDTO, Integer, Integer, BigDecimal)} only instead of
* adding a line with a new item, the quantity is moved to a new line using the same item (but a different price).
*
* @param order order containing lines to swap
* @param itemId item id
* @param quantity target quantity, items will be swapped if the existing order quantity exceeds this value.
* @param price price for the new line
* @return order with swapped lines
*/
public static OrderDTO swapExcess(OrderDTO order, Integer itemId, BigDecimal quantity, BigDecimal price) {
throw new UnsupportedOperationException("Swap excess not yet implemented.");
}
/**
* Merges two orders together to create a new order. The first orders attributes (active dates, status etc.)
* will be used for the newly merged order, except where null. If an attribute is null then the value
* from the second order will be used.
*
* Order lines from the two orders are merged together in the same manner as
* {@link #merge(OrderLineDTO, OrderLineDTO)}.
*
* @param order1 order to merge
* @param order2 other order to merge
* @return merged order
*/
public static OrderDTO merge(OrderDTO order1, OrderDTO order2) {
OrderDTO merged = new OrderDTO(order1);
if (merged.getBaseUserByUserId() == null) merged.setBaseUserByUserId(order2.getBaseUserByUserId());
if (merged.getCurrency() == null) merged.setCurrency(order2.getCurrency());
if (merged.getOrderStatus() == null) merged.setOrderStatus(order2.getOrderStatus());
if (merged.getOrderPeriod() == null) merged.setOrderPeriod(order2.getOrderPeriod());
if (merged.getOrderBillingType() == null) merged.setOrderBillingType(order2.getOrderBillingType());
if (merged.getActiveSince() == null) merged.setActiveSince(order2.getActiveSince());
if (merged.getActiveUntil() == null) merged.setActiveUntil(order2.getActiveUntil());
if (merged.getCycleStarts() == null) merged.setCycleStarts(order2.getCycleStarts());
if (merged.getCreateDate() == null) merged.setCreateDate(order2.getCreateDate());
if (merged.getNextBillableDay() == null) merged.setNextBillableDay(order2.getNextBillableDay());
if (merged.getLastNotified() == null) merged.setLastNotified(order2.getLastNotified());
if (merged.getNotificationStep() == null) merged.setNotificationStep(order2.getNotificationStep());
if (merged.getDueDateUnitId() == null) merged.setDueDateUnitId(order2.getDueDateUnitId());
if (merged.getDueDateValue() == null) merged.setDueDateValue(order2.getDueDateValue());
if (merged.getDfFm() == null) merged.setDfFm(order2.getDfFm());
if (merged.getAnticipatePeriods() == null) merged.setAnticipatePeriods(order2.getAnticipatePeriods());
if (merged.getOwnInvoice() == null) merged.setOwnInvoice(order2.getOwnInvoice());
if (merged.getNotes() == null) merged.setNotes(order2.getNotes());
if (merged.getNotesInInvoice() == null) merged.setNotesInInvoice(order2.getNotesInInvoice());
if (merged.getIsCurrent() == null) merged.setIsCurrent(order2.getIsCurrent());
merged.getOrderProcesses().clear();
merged.setId(0);
merged.setDeleted(0);
merged.getLines().addAll(order2.getLines());
for (OrderLineDTO line : merged.getLines()) {
line.setId(0);
}
return flatten(merged);
}
/**
* Merges the given order lines together. The merged order line will have a
* quantity and amount that is the sum of the original two lines. The merged line
* will not be associated with an order.
*
* The first line's attributes (description, item id etc.) will be used for the
* newly merged line, except where null. If an attribute is null the the value
* from the second line will be used.
*
* The unit price may no longer match the merged line total if the original two
* lines had a different unit prices.
*
* @param line1 line to merge
* @param line2 other line to merge
* @return merged order line
*/
public static OrderLineDTO merge(OrderLineDTO line1, OrderLineDTO line2) {
OrderLineDTO merged = new OrderLineDTO(line1);
merged.setAmount(add(line1.getAmount(), line2.getAmount()));
merged.setQuantity(add(line1.getQuantity(), line2.getQuantity()));
if (merged.getOrderLineType() == null) merged.setOrderLineType(line2.getOrderLineType());
if (merged.getItem() == null) merged.setItem(line2.getItem());
if (merged.getPrice() == null) merged.setPrice(line2.getPrice());
if (merged.getCreateDatetime() == null) merged.setCreateDatetime(line2.getCreateDatetime());
if (merged.getDescription() == null) merged.setDescription(line2.getDescription());
if (merged.getUseItem() == null) merged.getUseItem();
merged.setPurchaseOrder(null);
merged.setId(0);
merged.setDeleted(0);
return line1;
}
/**
* Removes all lines from the order with a matching item ID.
*
* @param order order to purge items from
* @param itemId item ID to remove
* @return purged order
*/
public static OrderDTO purge(OrderDTO order, Integer itemId) {
for (Iterator<OrderLineDTO> it = order.getLines().iterator(); it.hasNext();) {
OrderLineDTO line = it.next();
if (line.getItemId().equals(itemId)) {
it.remove();
}
}
return order;
}
/**
* Removes all lines from the order with an item ID contained in the given list of IDs.
*
* @param order order to purge items from
* @param itemIds item IDs to remove
* @return purged order
*/
public static OrderDTO purge(OrderDTO order, Collection<Integer> itemIds) {
for (Iterator<OrderLineDTO> it = order.getLines().iterator(); it.hasNext();) {
OrderLineDTO line = it.next();
if (itemIds.contains(line.getItemId())) {
it.remove();
}
}
return order;
}
/**
* Marks all lines from the order with a matching item ID as deleted.
*
* @param order order to delete lines from
* @param itemId item ID to delete
* @return order with line deleted
*/
public static OrderDTO delete(OrderDTO order, Integer itemId) {
for (OrderLineDTO line : order.getLines()) {
if (line.getItemId().equals(itemId)) {
line.setDeleted(1);
}
}
return order;
}
/**
* Marks all lines from the order with an item ID contained in the given list of IDs.
*
* @param order order to delete lines from
* @param itemIds item IDs to delete
* @return order with line deleted
*/
public static OrderDTO delete(OrderDTO order, Collection<Integer> itemIds) {
for (OrderLineDTO line : order.getLines()) {
if (itemIds.contains(line.getItemId())) {
line.setDeleted(1);
}
}
return order;
}
/**
* Flattens an order by removing all empty order lines, and merging all order
* lines down so that there are no duplicate item IDs.
*
* @param order order to flatten
* @return flattened order
*/
public static OrderDTO flatten(OrderDTO order) {
// remove all empty lines
for (Iterator<OrderLineDTO> it = order.getLines().iterator(); it.hasNext();) {
OrderLineDTO line = it.next();
if (line.getDeleted() == 1
|| (line.getQuantity().compareTo(BigDecimal.ZERO) == 0 && line.getAmount().compareTo(BigDecimal.ZERO) == 0)) {
it.remove();
}
}
// merge lines with the same item
List<OrderLineDTO> mergedLines = new ArrayList<OrderLineDTO>();
for (OrderLineDTO line : order.getLines()) {
OrderLineDTO merged = find(mergedLines, line.getItemId(), line.getPrice());
if (merged == null) {
// new line, add to list of merged lines
mergedLines.add(line);
} else {
// existing line, merge with line from list
mergedLines.remove(merged);
mergedLines.add(merge(merged, line));
}
}
order.setLines(mergedLines);
return order;
}
/**
* Returns all order lines from the given order where the item ID matches.
*
* @param order order
* @param itemId item id of lines to collect
* @return list of collected order lines, empty list if none found.
*/
public static List<OrderLineDTO> collect(OrderDTO order, Integer itemId) {
List<OrderLineDTO> lines = new ArrayList<OrderLineDTO>();
for (OrderLineDTO line : order.getLines()) {
if (line.getItemId().equals(itemId) && line.getDeleted() == 0) {
lines.add(line);
}
}
return lines;
}
/**
* Returns the line matching the given item ID and price. If no price is given (price is null),
* then the first line with a matching item ID will be returned.
*
* @param lines lines
* @param itemId item ID to find
* @param price optional item price
* @return found line, null if no line found
*/
public static OrderLineDTO find(Collection<OrderLineDTO> lines, Integer itemId, BigDecimal price) {
for (OrderLineDTO line : lines) {
if (line.getItemId().equals(itemId)) {
if (price != null) {
if (line.getPrice().compareTo(price) == 0) {
return line;
}
} else {
return line;
}
}
}
return null;
}
/**
* Converts an OrderDTO to use a thread-safe <code>CopyOnWriteArrayList</code> for order lines instead
* of an <code>ArrayList</code>. This allows safe concurrent modification of the order lines during
* iteration.
*
* You can also use Collections.synchronizedList, however the this method will provide better
* performance unless the number of writes to the list vastly outnumber the reads.
*
* @param order order to synchronize
*/
public static void synchronizeOrderLines(OrderDTO order) {
order.setLines(new CopyOnWriteArrayList<OrderLineDTO>(order.getLines()));
}
/**
* Removes synchronization on the OrderDTO order lines collection, copying the elements back
* into an <code>ArrayList</code>.
*
* @param order order to desynchronize
*/
public static void desynchronizeOrderLines(OrderDTO order) {
order.setLines(new ArrayList<OrderLineDTO>(order.getLines()));
}
/**
* Null safe convenience method for adding two BigDecimal objects.
*
* @param a big decimal two
* @param b big decimal one
* @return sum of the two big decimals
*/
private static BigDecimal add(BigDecimal a, BigDecimal b) {
if (a == null) return b;
if (b == null) return a;
return a.add(b);
}
}