killbill-aplcache

Changes

account/pom.xml 2(+1 -1)

api/pom.xml 2(+1 -1)

beatrix/pom.xml 2(+1 -1)

catalog/pom.xml 2(+1 -1)

currency/pom.xml 2(+1 -1)

invoice/pom.xml 2(+1 -1)

jaxrs/pom.xml 2(+1 -1)

junction/pom.xml 2(+1 -1)

NEWS 6(+6 -0)

overdue/pom.xml 2(+1 -1)

payment/pom.xml 2(+1 -1)

pom.xml 4(+2 -2)

profiles/pom.xml 2(+1 -1)

tenant/pom.xml 2(+1 -1)

usage/pom.xml 2(+1 -1)

util/pom.xml 2(+1 -1)

Details

account/pom.xml 2(+1 -1)

diff --git a/account/pom.xml b/account/pom.xml
index df1ec85..ba0b8a9 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-account</artifactId>

api/pom.xml 2(+1 -1)

diff --git a/api/pom.xml b/api/pom.xml
index 0435593..8936827 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-internal-api</artifactId>

beatrix/pom.xml 2(+1 -1)

diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index 689180a..c752c0d 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-beatrix</artifactId>
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
index ec6768b..8fd9f9d 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationInvoiceWithRepairLogic.java
@@ -19,11 +19,14 @@
 package org.killbill.billing.beatrix.integration;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
+import javax.inject.Inject;
+
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
 import org.killbill.billing.account.api.Account;
@@ -37,8 +40,14 @@ import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
+import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
+import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.invoice.api.InvoiceStatus;
+import org.killbill.billing.invoice.dao.InvoiceDao;
+import org.killbill.billing.invoice.dao.InvoiceItemModelDao;
+import org.killbill.billing.invoice.dao.InvoiceModelDao;
 import org.killbill.billing.payment.api.Payment;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionStatus;
@@ -52,6 +61,10 @@ import static org.testng.Assert.assertNotNull;
 
 public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
 
+    @Inject
+    protected InvoiceDao invoiceDao;
+
+
     @AfterMethod(groups = "slow")
     public void afterMethod() throws Exception {
         super.afterMethod();
@@ -607,4 +620,130 @@ public class TestIntegrationInvoiceWithRepairLogic extends TestIntegrationBase {
         refundPaymentWithInvoiceItemAdjAndCheckForCompletion(account, payment1, iias, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.INVOICE_ADJUSTMENT);
         checkNoMoreInvoiceToGenerate(account);
     }
+
+    @Test(groups = "slow")
+    public void testWithSuperflousRepairedItems() throws Exception {
+
+        // We take april as it has 30 days (easier to play with BCD)
+        final LocalDate today = new LocalDate(2012, 4, 1);
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDay(today);
+
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+        final String pricelistName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+        //
+
+        final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.INVOICE, NextEvent.BLOCK);
+        assertNotNull(bpEntitlement);
+
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+        assertEquals(invoices.size(), 1);
+        ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, BigDecimal.ZERO));
+        invoiceChecker.checkInvoice(invoices.get(0).getId(), callContext, toBeChecked);
+
+        //
+        // Check we get the first invoice at the phase event
+        //
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        // Move the clock to 2012-05-02
+        clock.addDays(31);
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+        assertEquals(invoices.size(), 2);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, BigDecimal.ZERO));
+        invoiceChecker.checkInvoice(invoices.get(0).getId(), callContext, toBeChecked);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        final Invoice lastInvoice = invoices.get(1);
+        invoiceChecker.checkInvoice(lastInvoice.getId(), callContext, toBeChecked);
+
+
+        //
+        // Let's add a bunch of items by hand to pretend we have lots of cancelling items that should be cleaned (data issue after a potential invoice bug)
+        //
+        // We test both full and partial repair.
+
+        /*
+        final UUID id, @Nullable final DateTime createdDate, final UUID accountId,
+                           @Nullable final Integer invoiceNumber, final LocalDate invoiceDate, final LocalDate targetDate,
+                           final Currency currency, final boolean migrated, final InvoiceStatus status, final boolean isParentInvoice
+         */
+        final InvoiceModelDao shellInvoice = new InvoiceModelDao(UUID.randomUUID(), lastInvoice.getCreatedDate(), lastInvoice.getAccountId(), null,
+                                                                 lastInvoice.getInvoiceDate(), lastInvoice.getTargetDate(), lastInvoice.getCurrency(), false, InvoiceStatus.COMMITTED, false);
+
+        final InvoiceItemModelDao recurring1 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.RECURRING, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                       bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), "", "shotgun-monthly", "shotgun-monthly-evergreen",
+                                                                       null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("249.95"), new BigDecimal("249.95"), account.getCurrency(), null);
+
+        final InvoiceItemModelDao repair1 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                    bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+                                                                    null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("-249.95"), new BigDecimal("-249.95"), account.getCurrency(), recurring1.getId());
+
+        final InvoiceItemModelDao recurring2 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.RECURRING, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                       bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), "", "shotgun-monthly", "shotgun-monthly-evergreen",
+                                                                       null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("249.95"), new BigDecimal("249.95"), account.getCurrency(), null);
+
+
+        final InvoiceItemModelDao repair21 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                     bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+                                                                     null, new LocalDate(2012, 5, 1), new LocalDate(2012, 5, 13), new BigDecimal("-100.95"), new BigDecimal("-100.95"), account.getCurrency(), recurring2.getId());
+
+        final InvoiceItemModelDao repair22 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                     bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+                                                                     null, new LocalDate(2012, 5, 13), new LocalDate(2012, 5, 22), new BigDecimal("-100"), new BigDecimal("-100"), account.getCurrency(), recurring2.getId());
+
+        final InvoiceItemModelDao repair23 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                     bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+                                                                     null, new LocalDate(2012, 5, 22), new LocalDate(2012, 6, 1), new BigDecimal("-49"), new BigDecimal("-49"), account.getCurrency(), recurring2.getId());
+
+
+
+        final InvoiceItemModelDao recurring3 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.RECURRING, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                       bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), "", "shotgun-monthly", "shotgun-monthly-evergreen",
+                                                                       null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("249.95"), new BigDecimal("249.95"), account.getCurrency(), null);
+
+
+        final InvoiceItemModelDao repair3 = new InvoiceItemModelDao(lastInvoice.getCreatedDate(), InvoiceItemType.REPAIR_ADJ, lastInvoice.getId(), lastInvoice.getAccountId(),
+                                                                    bpEntitlement.getBundleId(), bpEntitlement.getBaseEntitlementId(), null, null, null,
+                                                                    null, new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), new BigDecimal("-249.95"), new BigDecimal("-249.95"), account.getCurrency(), recurring3.getId());
+
+        List<InvoiceItemModelDao> newItems = new ArrayList<InvoiceItemModelDao>();
+        newItems.add(recurring1);
+        newItems.add(repair1);
+        newItems.add(recurring2);
+        newItems.add(repair21);
+        newItems.add(repair22);
+        newItems.add(repair23);
+        newItems.add(recurring3);
+        newItems.add(repair3);
+        invoiceDao.createInvoice(shellInvoice, newItems, false, new FutureAccountNotifications(new HashMap<UUID, List<SubscriptionNotification>>()), internalCallContext);
+
+
+        // Move ahead one month, verify nothing from previous data was generated
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.addMonths(1);
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+        assertEquals(invoices.size(), 3);
+
+        toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        invoiceChecker.checkInvoice(invoices.get(2).getId(), callContext, toBeChecked);
+
+
+
+    }
+
 }

catalog/pom.xml 2(+1 -1)

diff --git a/catalog/pom.xml b/catalog/pom.xml
index 3a28cf4..49aad72 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-catalog</artifactId>
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
index 19ceed6..84fe628 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
@@ -308,8 +308,8 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements 
     @Override
     public String toString() {
         return "DefaultPlan [name=" + name + ", effectiveDateForExistingSubscriptions="
-               + effectiveDateForExistingSubscriptions + ", product=" + product + ", initialPhases="
-               + Arrays.toString(initialPhases) + ", finalPhase=" + finalPhase + ", plansAllowedInBundle="
+               + effectiveDateForExistingSubscriptions + ", product=" + product.getName() + ", initialPhases="
+               + Arrays.toString(initialPhases) + ", finalPhase=" + finalPhase.getName() + ", plansAllowedInBundle="
                + plansAllowedInBundle + "]";
     }
 
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java
index ee2226a..bf451af 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java
@@ -242,4 +242,17 @@ public class DefaultPlanPhase extends ValidatingConfig<StandaloneCatalog> implem
         //result = 31 * result + (usages != null ? Arrays.hashCode(usages) : 0);
         return result;
     }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("DefaultPlanPhase{");
+        sb.append("type=").append(type);
+        sb.append(", duration=").append(duration);
+        sb.append(", fixed=").append(fixed);
+        sb.append(", recurring=").append(recurring);
+        sb.append(", usages=").append(Arrays.toString(usages));
+        sb.append(", plan=").append(plan.getName());
+        sb.append('}');
+        return sb.toString();
+    }
 }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
index fbf5591..83f07ac 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
@@ -36,11 +36,9 @@ import javax.xml.bind.annotation.XmlRootElement;
 
 import org.joda.time.DateTime;
 import org.killbill.billing.ErrorCode;
-import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingAlignment;
 import org.killbill.billing.catalog.api.BillingMode;
-import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.Currency;
@@ -131,6 +129,10 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
         public Plan findPlan(final StandaloneCatalog catalog) throws CatalogApiException {
             return catalog.createOrFindCurrentPlan(spec, overrides);
         }
+
+        public PlanSpecifier getSpec() {
+            return spec;
+        }
     }
 
     private CatalogPlanEntry findCatalogPlanEntry(final PlanRequestWrapper wrapper,
@@ -169,7 +171,12 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
             }
         }
 
-        throw new CatalogApiException(ErrorCode.CAT_NO_CATALOG_FOR_GIVEN_DATE, requestedDate.toDate().toString());
+        final PlanSpecifier spec = wrapper.getSpec();
+        throw new CatalogApiException(ErrorCode.CAT_PLAN_NOT_FOUND,
+                                      spec.getPlanName() != null ? spec.getPlanName() : "undefined",
+                                      spec.getProductName() != null ? spec.getProductName() : "undefined",
+                                      spec.getBillingPeriod() != null ? spec.getBillingPeriod() : "undefined",
+                                      spec.getPriceListName() != null ? spec.getPriceListName() : "undefined");
     }
 
     private static class CatalogPlanEntry {

currency/pom.xml 2(+1 -1)

diff --git a/currency/pom.xml b/currency/pom.xml
index fc2b5a7..3278f26 100644
--- a/currency/pom.xml
+++ b/currency/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-currency</artifactId>
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 02d02a8..20c9c9b 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-entitlement</artifactId>

invoice/pom.xml 2(+1 -1)

diff --git a/invoice/pom.xml b/invoice/pom.xml
index ec47467..24b2082 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-invoice</artifactId>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
index 48e946a..e3f9af0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
@@ -21,6 +21,7 @@ package org.killbill.billing.invoice.tree;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -282,18 +283,19 @@ public class ItemsNodeInterval extends NodeInterval {
     //   it has no more leaves and no more items.
     // Case B - This is a bit more involved: We look for full repair that happened in pieces; this will translate to an ADD element of a NodeInterval,
     // whose children completely map the interval (isPartitionedByChildren) and where each child will have a CANCEL item pointing to the ADD.
-    // When we detect such nodes, we delete both the ADD in the parent interval and the CANCEL in the children
+    // When we detect such nodes, we delete both the ADD in the parent interval and the CANCEL in the children (and cleanup the interval if it does not have items)
     //
     private void pruneTree() {
         walkTree(new WalkCallback() {
             @Override
             public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
 
-                if(curNode.isRoot()) {
+                if (curNode.isRoot()) {
                     return;
                 }
 
                 final ItemsInterval curNodeItems = ((ItemsNodeInterval) curNode).getItemsInterval();
+
                 // Case A:
                 final boolean isEmpty = curNodeItems.mergeCancellingPairs();
                 if (isEmpty && curNode.getLeftChild() == null) {
@@ -305,14 +307,22 @@ public class ItemsNodeInterval extends NodeInterval {
                 }
 
                 // Case B -- look for such case, and if found (foundFullRepairByParts) we fix them below.
-                final Iterator<Item> it =  curNodeItems.get_ADD_items().iterator();
+                List<Item> curNodeItemsToBeRemoved = null;
+                final Iterator<Item> it = curNodeItems.get_ADD_items().iterator();
+                // For each item on this curNode interval we check if there is a matching set of CANCEL items on the children (resulting in completely cancelling that item).
                 while (it.hasNext()) {
 
                     final Item curAddItem = it.next();
 
+                    //
+                    // We already know the children partition fully the 'curNode' interval, we just need to see if for each piece
+                    // we find a matching CANCEL item pointing to this 'curAddItem'
+                    //
                     NodeInterval curChild = curNode.getLeftChild();
-                    Map<ItemsInterval, Item> toBeRemoved = new HashMap<ItemsInterval, Item>();
-                    boolean foundFullRepairByParts = true;
+                    Map<ItemsInterval, Item> childrenCancellingToBeRemoved = new HashMap<ItemsInterval, Item>();
+
+                    // Note that because of previous iterations, curChild could now be null so we need to initialize the foundFullRepairByParts based on that new state.
+                    boolean foundFullRepairByParts = curChild != null;
                     while (curChild != null) {
                         final ItemsInterval curChildItems = ((ItemsNodeInterval) curChild).getItemsInterval();
                         Item cancellingItem = curChildItems.getCancelledItemIfExists(curAddItem.getId());
@@ -320,20 +330,27 @@ public class ItemsNodeInterval extends NodeInterval {
                             foundFullRepairByParts = false;
                             break;
                         }
-                        toBeRemoved.put(curChildItems, cancellingItem);
+                        childrenCancellingToBeRemoved.put(curChildItems, cancellingItem);
                         curChild = curChild.getRightSibling();
                     }
 
                     if (foundFullRepairByParts) {
-                        for (ItemsInterval curItemsInterval : toBeRemoved.keySet()) {
-                            curItemsInterval.remove(toBeRemoved.get(curItemsInterval));
+                        for (ItemsInterval curItemsInterval : childrenCancellingToBeRemoved.keySet()) {
+                            curItemsInterval.remove(childrenCancellingToBeRemoved.get(curItemsInterval));
                             if (curItemsInterval.size() == 0) {
                                 curNode.removeChild(curItemsInterval.getNodeInterval());
                             }
                         }
-                        curNodeItems.remove(curAddItem);
+                        if (curNodeItemsToBeRemoved == null) {
+                            curNodeItemsToBeRemoved = new ArrayList<Item>();
+                        }
+                        curNodeItemsToBeRemoved.add(curAddItem);
                     }
                 }
+                // Finally Execute the removal of the curNodeItems outside of the upper while loop so as to not trigger ConcurrentModificationException (see #641)
+                for (Item curNodeItemsRemoval : curNodeItemsToBeRemoved) {
+                    curNodeItems.remove(curNodeItemsRemoval);
+                }
             }
         });
     }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
index 102f9c7..edc9cf5 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -394,6 +394,189 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
         verifyResult(tree.getView(), expectedResult);
     }
 
+    // Will test the case A from ItemsNodeInterval#prune logic (when we delete a node while walking the tree)
+    @Test(groups = "fast")
+    public void testFullRepairPruneLogic1() {
+
+        final LocalDate startDate1 = new LocalDate(2015, 1, 1);
+        final LocalDate endDate1 = new LocalDate(2015, 2, 1);
+
+
+        final LocalDate startDate2 = endDate1;
+        final LocalDate endDate2 = new LocalDate(2015, 3, 1);
+
+        final LocalDate startDate3 = endDate2;
+        final LocalDate endDate3 = new LocalDate(2015, 4, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate1, endDate1, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate2, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repairMonthly2 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate2, endDate2, new BigDecimal("-12.00"), currency, monthly2.getId());
+        final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate3, endDate3, monthlyAmount, monthlyRate, currency);
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly1);
+        expectedResult.add(monthly3);
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+        tree.addItem(monthly1);
+        tree.addItem(monthly2);
+        tree.addItem(repairMonthly2);
+        tree.addItem(monthly3);
+
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+
+    // Will test the case A from ItemsNodeInterval#prune logic (an item is left on the interval)
+    @Test(groups = "fast")
+    public void testFullRepairPruneLogic2() {
+
+        final LocalDate startDate1 = new LocalDate(2015, 1, 1);
+        final LocalDate endDate1 = new LocalDate(2015, 2, 1);
+
+
+        final LocalDate startDate2 = endDate1;
+        final LocalDate endDate2 = new LocalDate(2015, 3, 1);
+
+        final LocalDate startDate3 = endDate2;
+        final LocalDate endDate3 = new LocalDate(2015, 4, 1);
+
+        final BigDecimal monthlyRateInit = new BigDecimal("12.00");
+        final BigDecimal monthlyAmountInit = monthlyRateInit;
+
+        final BigDecimal monthlyRateFinal = new BigDecimal("15.00");
+        final BigDecimal monthlyAmountFinal = monthlyRateFinal;
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate1, endDate1, monthlyRateInit, monthlyAmountInit, currency);
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate2, monthlyRateInit, monthlyAmountInit, currency);
+        final InvoiceItem repairMonthly2 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate2, endDate2, new BigDecimal("-12.00"), currency, monthly2.getId());
+
+        final InvoiceItem monthly2New = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate2, endDate2, monthlyRateFinal, monthlyAmountFinal, currency);
+
+        final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate3, endDate3, monthlyRateFinal, monthlyAmountFinal, currency);
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly1);
+        expectedResult.add(monthly2New);
+        expectedResult.add(monthly3);
+
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+        tree.addItem(monthly1);
+        tree.addItem(monthly2);
+        tree.addItem(repairMonthly2);
+        tree.addItem(monthly2New);
+        tree.addItem(monthly3);
+
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+
+    }
+
+
+    // Will test the case B from ItemsNodeInterval#prune logic
+    @Test(groups = "fast")
+    public void testFullRepairByPartsPruneLogic1() {
+
+        final LocalDate startDate = new LocalDate(2015, 2, 1);
+        final LocalDate intermediate1 = new LocalDate(2015, 2, 8);
+        final LocalDate intermediate2 = new LocalDate(2015, 2, 16);
+        final LocalDate intermediate3 = new LocalDate(2015, 2, 24);
+        final LocalDate endDate = new LocalDate(2015, 3, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repair11 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, intermediate1, new BigDecimal("3.00"), currency, monthly1.getId());
+        final InvoiceItem repair12 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate1, intermediate2, new BigDecimal("3.00"), currency, monthly1.getId());
+        final InvoiceItem repair13 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate2, intermediate3, new BigDecimal("3.00"), currency, monthly1.getId());
+        final InvoiceItem repair14 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate3, endDate, new BigDecimal("3.00"), currency, monthly1.getId());
+
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repair21 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, intermediate1, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair22 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate1, intermediate2, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair23 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate2, intermediate3, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair24 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate3, endDate, new BigDecimal("3.00"), currency, monthly2.getId());
+
+        final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly3);
+
+        // First test with items in order
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+        tree.addItem(monthly1);
+        tree.addItem(repair11);
+        tree.addItem(repair12);
+        tree.addItem(repair13);
+        tree.addItem(repair14);
+
+        tree.addItem(monthly2);
+        tree.addItem(repair21);
+        tree.addItem(repair22);
+        tree.addItem(repair23);
+        tree.addItem(repair24);
+
+        tree.addItem(monthly3);
+
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+    // Will test the case A and B from ItemsNodeInterval#prune logic
+    @Test(groups = "fast")
+    public void testFullRepairByPartsPruneLogic2() {
+
+        final LocalDate startDate = new LocalDate(2015, 2, 1);
+        final LocalDate intermediate1 = new LocalDate(2015, 2, 8);
+        final LocalDate intermediate2 = new LocalDate(2015, 2, 16);
+        final LocalDate intermediate3 = new LocalDate(2015, 2, 24);
+        final LocalDate endDate = new LocalDate(2015, 3, 1);
+
+        final BigDecimal monthlyRate = new BigDecimal("12.00");
+        final BigDecimal monthlyAmount = monthlyRate;
+
+
+        final InvoiceItem monthly1 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repair11 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, endDate, new BigDecimal("-12.00"), currency, monthly1.getId());
+
+        final InvoiceItem monthly2 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+        final InvoiceItem repair21 = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, intermediate1, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair22 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate1, intermediate2, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair23 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate2, intermediate3, new BigDecimal("3.00"), currency, monthly2.getId());
+        final InvoiceItem repair24 = new RepairAdjInvoiceItem(invoiceId, accountId, intermediate3, endDate, new BigDecimal("3.00"), currency, monthly2.getId());
+
+        final InvoiceItem monthly3 = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, monthlyAmount, monthlyRate, currency);
+
+
+        final List<InvoiceItem> expectedResult = Lists.newLinkedList();
+        expectedResult.add(monthly3);
+
+        // First test with items in order
+        final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+        tree.addItem(monthly1);
+        tree.addItem(repair11);
+
+        tree.addItem(monthly2);
+        tree.addItem(repair21);
+        tree.addItem(repair22);
+        tree.addItem(repair23);
+        tree.addItem(repair24);
+
+        tree.addItem(monthly3);
+
+        tree.build();
+        verifyResult(tree.getView(), expectedResult);
+    }
+
+
+
     @Test(groups = "fast")
     public void testMergeWithNoExisting() {
 

jaxrs/pom.xml 2(+1 -1)

diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index 4fc64e3..45a9f4b 100644
--- a/jaxrs/pom.xml
+++ b/jaxrs/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-jaxrs</artifactId>
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
index ef004a4..bbcecc3 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -415,13 +415,13 @@ public class AccountResource extends JaxRsResourceBase {
         final Callable<List<SubscriptionBundle>> bundlesCallable = new Callable<List<SubscriptionBundle>>() {
             @Override
             public List<SubscriptionBundle> call() throws Exception {
-                return subscriptionApi.getSubscriptionBundlesForAccountId(account.getId(), tenantContext);
+                return subscriptionApi.getSubscriptionBundlesForAccountId(accountId, tenantContext);
             }
         };
         final Callable<List<Invoice>> invoicesCallable = new Callable<List<Invoice>>() {
             @Override
             public List<Invoice> call() throws Exception {
-                return invoiceApi.getInvoicesByAccount(account.getId(), false, tenantContext);
+                return invoiceApi.getInvoicesByAccount(accountId, false, tenantContext);
             }
         };
         final Callable<List<InvoicePayment>> invoicePaymentsCallable = new Callable<List<InvoicePayment>>() {
@@ -452,59 +452,76 @@ public class AccountResource extends JaxRsResourceBase {
         AccountAuditLogs accountAuditLogs = null;
 
         if (parallel) {
-
             final ExecutorService executor = jaxrsExecutors.getJaxrsExecutorService();
             final Future<List<SubscriptionBundle>> futureBundlesCallable = executor.submit(bundlesCallable);
             final Future<List<Invoice>> futureInvoicesCallable = executor.submit(invoicesCallable);
             final Future<List<InvoicePayment>> futureInvoicePaymentsCallable = executor.submit(invoicePaymentsCallable);
             final Future<List<Payment>> futurePaymentsCallable = executor.submit(paymentsCallable);
             final Future<AccountAuditLogs> futureAuditsCallable = executor.submit(auditsCallable);
-
-            try {
-                long ini = System.currentTimeMillis();
-                do {
-                    bundles = (bundles == null) ? runCallableAndHandleTimeout(futureBundlesCallable, 100) : bundles;
-                    invoices = (invoices == null) ? runCallableAndHandleTimeout(futureInvoicesCallable, 100) : invoices;
-                    invoicePayments = (invoicePayments == null) ? runCallableAndHandleTimeout(futureInvoicePaymentsCallable, 100) : invoicePayments;
-                    payments = (payments == null) ? runCallableAndHandleTimeout(futurePaymentsCallable, 100) : payments;
-                    accountAuditLogs = (accountAuditLogs == null) ? runCallableAndHandleTimeout(futureAuditsCallable, 100) : accountAuditLogs;
-                } while ((System.currentTimeMillis() - ini < jaxrsConfig.getJaxrsTimeout().getMillis()) &&
-                         (bundles == null || invoices == null || invoicePayments == null || payments == null || accountAuditLogs == null));
-
-                if (bundles == null || invoices == null || invoicePayments == null || payments == null || accountAuditLogs == null) {
-                    Response.status(Status.SERVICE_UNAVAILABLE).build();
-                }
-            } catch (InterruptedException e) {
-                handleCallableException(e, ImmutableList.<Future>of(futureBundlesCallable, futureInvoicesCallable, futureInvoicePaymentsCallable, futurePaymentsCallable, futureAuditsCallable));
-            } catch (ExecutionException e) {
-                handleCallableException(e.getCause(), ImmutableList.<Future>of(futureBundlesCallable, futureInvoicesCallable, futureInvoicePaymentsCallable, futurePaymentsCallable, futureAuditsCallable));
+            final ImmutableList<Future> toBeCancelled = ImmutableList.<Future>of(futureBundlesCallable, futureInvoicesCallable, futureInvoicePaymentsCallable, futurePaymentsCallable, futureAuditsCallable);
+            final int timeoutMsec = 100;
+
+            final long ini = System.currentTimeMillis();
+            do {
+                bundles = (bundles == null) ? waitOnFutureAndHandleTimeout("bundles", futureBundlesCallable, timeoutMsec, toBeCancelled) : bundles;
+                invoices = (invoices == null) ? waitOnFutureAndHandleTimeout("invoices", futureInvoicesCallable, timeoutMsec, toBeCancelled) : invoices;
+                invoicePayments = (invoicePayments == null) ? waitOnFutureAndHandleTimeout("invoicePayments", futureInvoicePaymentsCallable, timeoutMsec, toBeCancelled) : invoicePayments;
+                payments = (payments == null) ? waitOnFutureAndHandleTimeout("payments", futurePaymentsCallable, timeoutMsec, toBeCancelled) : payments;
+                accountAuditLogs = (accountAuditLogs == null) ? waitOnFutureAndHandleTimeout("accountAuditLogs", futureAuditsCallable, timeoutMsec, toBeCancelled) : accountAuditLogs;
+            } while ((System.currentTimeMillis() - ini < jaxrsConfig.getJaxrsTimeout().getMillis()) &&
+                     (bundles == null || invoices == null || invoicePayments == null || payments == null || accountAuditLogs == null));
+
+            if (bundles == null || invoices == null || invoicePayments == null || payments == null || accountAuditLogs == null) {
+                Response.status(Status.SERVICE_UNAVAILABLE).build();
             }
-
         } else {
-            try {
-                invoices = invoicesCallable.call();
-                payments = paymentsCallable.call();
-                bundles = bundlesCallable.call();
-                accountAuditLogs = auditsCallable.call();
-                invoicePayments = invoicePaymentsCallable.call();
-            } catch (Exception e) {
-                handleCallableException(e);
-            }
+            invoices = runCallable("invoices", invoicesCallable);
+            payments = runCallable("payments", paymentsCallable);
+            bundles = runCallable("bundles", bundlesCallable);
+            accountAuditLogs = runCallable("accountAuditLogs", auditsCallable);
+            invoicePayments = runCallable("invoicePayments", invoicePaymentsCallable);
         }
 
         json = new AccountTimelineJson(account, invoices, payments, invoicePayments, bundles, accountAuditLogs);
         return Response.status(Status.OK).entity(json).build();
     }
 
-    private <T> T runCallableAndHandleTimeout(final Future<T> future, final long timeoutMsec) throws ExecutionException, InterruptedException {
+    private <T> T waitOnFutureAndHandleTimeout(final String logSuffix, final Future<T> future, final long timeoutMsec, final Iterable<Future> toBeCancelled) throws PaymentApiException, AccountApiException, InvoiceApiException, SubscriptionApiException {
+        try {
+            return waitOnFutureAndHandleTimeout(future, timeoutMsec);
+        } catch (final InterruptedException e) {
+            log.warn("InterruptedException while retrieving {}", logSuffix, e);
+            handleCallableException(e, toBeCancelled);
+        } catch (final ExecutionException e) {
+            log.warn("ExecutionException while retrieving {}", logSuffix, e);
+            handleCallableException(e.getCause(), toBeCancelled);
+        }
+
+        // Never reached
+        return null;
+    }
+
+    private <T> T waitOnFutureAndHandleTimeout(final Future<T> future, final long timeoutMsec) throws ExecutionException, InterruptedException {
         try {
             return future.get(timeoutMsec, TimeUnit.MILLISECONDS);
-        } catch (TimeoutException e) {
+        } catch (final TimeoutException e) {
             return null;
         }
     }
 
-    private void handleCallableException(final Throwable causeOrException, final List<Future> toBeCancelled) throws AccountApiException, SubscriptionApiException, PaymentApiException, InvoiceApiException {
+    private <T> T runCallable(final String logSuffix, final Callable<T> callable) throws PaymentApiException, AccountApiException, InvoiceApiException, SubscriptionApiException {
+        try {
+            return callable.call();
+        } catch (final Exception e) {
+            log.warn("InterruptedException while retrieving {}", logSuffix, e);
+            handleCallableException(e);
+        }
+
+        // Never reached
+        return null;
+    }
+
+    private void handleCallableException(final Throwable causeOrException, final Iterable<Future> toBeCancelled) throws AccountApiException, SubscriptionApiException, PaymentApiException, InvoiceApiException {
         for (final Future f : toBeCancelled) {
             f.cancel(true);
         }

junction/pom.xml 2(+1 -1)

diff --git a/junction/pom.xml b/junction/pom.xml
index 8e003a7..6206cee 100644
--- a/junction/pom.xml
+++ b/junction/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-junction</artifactId>

NEWS 6(+6 -0)

diff --git a/NEWS b/NEWS
index 8bc0c7b..bafdcf0 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+0.17.8
+    See https://github.com/killbill/killbill/releases/tag/killbill-0.17.8
+
+0.17.7
+    See https://github.com/killbill/killbill/releases/tag/killbill-0.17.7
+
 0.17.6
     See https://github.com/killbill/killbill/releases/tag/killbill-0.17.6
 

overdue/pom.xml 2(+1 -1)

diff --git a/overdue/pom.xml b/overdue/pom.xml
index 31afc8d..2abb00d 100644
--- a/overdue/pom.xml
+++ b/overdue/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-overdue</artifactId>

payment/pom.xml 2(+1 -1)

diff --git a/payment/pom.xml b/payment/pom.xml
index 29c1b08..07af64e 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-payment</artifactId>
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
index b83cc69..22e12ff 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
@@ -27,6 +27,7 @@ import javax.inject.Inject;
 
 import org.killbill.automaton.MissingEntryException;
 import org.killbill.automaton.State;
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.account.api.AccountInternalApi;
@@ -47,6 +48,7 @@ import org.killbill.billing.payment.dao.PaymentDao;
 import org.killbill.billing.payment.dao.PaymentModelDao;
 import org.killbill.billing.payment.dao.PluginPropertySerializer;
 import org.killbill.billing.payment.dao.PluginPropertySerializer.PluginPropertySerializerException;
+import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
 import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
 import org.killbill.billing.tag.TagInternalApi;
 import org.killbill.billing.util.callcontext.CallContext;
@@ -289,7 +291,15 @@ public class PluginControlPaymentProcessor extends ProcessorBase {
         } catch (final AccountApiException e) {
             log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
         } catch (final PaymentApiException e) {
-            log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
+            // Log exception unless nothing left to be paid
+            if (e.getCode() == ErrorCode.PAYMENT_PLUGIN_API_ABORTED.getCode() &&
+                paymentControlPluginNames != null &&
+                paymentControlPluginNames.size() == 1 &&
+                InvoicePaymentControlPluginApi.PLUGIN_NAME.equals(paymentControlPluginNames.get(0))) {
+                log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'. Invoice has already been paid", attemptId, toPluginNamesOnError(paymentControlPluginNames));
+            } else {
+                log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
+            }
         } catch (final PluginPropertySerializerException e) {
             log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
         } catch (final MissingEntryException e) {

pom.xml 4(+2 -2)

diff --git a/pom.xml b/pom.xml
index 7092424..cfa80b9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,10 +21,10 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.135-SNAPSHOT</version>
+        <version>0.136-SNAPSHOT</version>
     </parent>
     <artifactId>killbill</artifactId>
-    <version>0.17.7-SNAPSHOT</version>
+    <version>0.17.9-SNAPSHOT</version>
     <packaging>pom</packaging>
     <name>killbill</name>
     <description>Library for managing recurring subscriptions and the associated billing</description>
diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml
index 2f15884..dda1488 100644
--- a/profiles/killbill/pom.xml
+++ b/profiles/killbill/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-profiles</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles-killbill</artifactId>
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccountTimeline.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccountTimeline.java
index 66eb996..b0a42f3 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccountTimeline.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAccountTimeline.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -26,6 +26,8 @@ import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
+import org.killbill.billing.client.KillBillClientException;
+import org.killbill.billing.client.RequestOptions;
 import org.killbill.billing.client.model.Account;
 import org.killbill.billing.client.model.AccountTimeline;
 import org.killbill.billing.client.model.AuditLog;
@@ -42,6 +44,10 @@ import org.killbill.billing.util.audit.ChangeType;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.HashMultimap;
+
+import static org.killbill.billing.jaxrs.resources.JaxrsResource.QUERY_PARALLEL;
+
 public class TestAccountTimeline extends TestJaxrsBase {
 
     private static final String PAYMENT_REQUEST_PROCESSOR = "PaymentRequestProcessor";
@@ -53,7 +59,7 @@ public class TestAccountTimeline extends TestJaxrsBase {
 
         final Account accountJson = createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoice();
 
-        final AccountTimeline timeline = killBillClient.getAccountTimeline(accountJson.getAccountId());
+        final AccountTimeline timeline = getAccountTimeline(accountJson.getAccountId(), AuditLevel.NONE);
         Assert.assertEquals(timeline.getPayments().size(), 1);
         Assert.assertEquals(timeline.getInvoices().size(), 2);
         Assert.assertEquals(timeline.getBundles().size(), 1);
@@ -113,7 +119,7 @@ public class TestAccountTimeline extends TestJaxrsBase {
     private void verifyPayments(final UUID accountId, final DateTime startTime, final DateTime endTime,
                                 final BigDecimal refundAmount, final BigDecimal chargebackAmount) throws Exception {
         for (final AuditLevel auditLevel : AuditLevel.values()) {
-            final AccountTimeline timeline = killBillClient.getAccountTimeline(accountId, auditLevel);
+            final AccountTimeline timeline = getAccountTimeline(accountId, auditLevel);
 
             Assert.assertEquals(timeline.getPayments().size(), 1);
             final InvoicePayment payment = timeline.getPayments().get(0);
@@ -183,7 +189,7 @@ public class TestAccountTimeline extends TestJaxrsBase {
 
     private void verifyInvoices(final UUID accountId, final DateTime startTime, final DateTime endTime) throws Exception {
         for (final AuditLevel auditLevel : AuditLevel.values()) {
-            final AccountTimeline timeline = killBillClient.getAccountTimeline(accountId, auditLevel);
+            final AccountTimeline timeline = getAccountTimeline(accountId, auditLevel);
 
             // Verify invoices
             Assert.assertEquals(timeline.getInvoices().size(), 3);
@@ -209,7 +215,7 @@ public class TestAccountTimeline extends TestJaxrsBase {
 
     private void verifyCredits(final UUID accountId, final DateTime startTime, final DateTime endTime, final BigDecimal creditAmount) throws Exception {
         for (final AuditLevel auditLevel : AuditLevel.values()) {
-            final AccountTimeline timeline = killBillClient.getAccountTimeline(accountId, auditLevel);
+            final AccountTimeline timeline = getAccountTimeline(accountId, auditLevel);
 
             // Verify credits
             final List<Credit> credits = timeline.getInvoices().get(1).getCredits();
@@ -229,7 +235,7 @@ public class TestAccountTimeline extends TestJaxrsBase {
 
     private void verifyBundles(final UUID accountId, final DateTime startTime, final DateTime endTime) throws Exception {
         for (final AuditLevel auditLevel : AuditLevel.values()) {
-            final AccountTimeline timeline = killBillClient.getAccountTimeline(accountId, auditLevel);
+            final AccountTimeline timeline = getAccountTimeline(accountId, auditLevel);
 
             // Verify bundles
             Assert.assertEquals(timeline.getBundles().size(), 1);
@@ -298,4 +304,17 @@ public class TestAccountTimeline extends TestJaxrsBase {
         Assert.assertEquals(auditLogJson.getComments(), comments);
         Assert.assertEquals(auditLogJson.getChangedBy(), changedBy);
     }
+
+    private AccountTimeline getAccountTimeline(final UUID accountId, final AuditLevel auditLevel) throws KillBillClientException {
+        final AccountTimeline accountTimeline = killBillClient.getAccountTimeline(accountId, auditLevel, RequestOptions.empty());
+
+        // Verify also the parallel path
+        final HashMultimap<String, String> queryParams = HashMultimap.<String, String>create();
+        queryParams.put(QUERY_PARALLEL, "true");
+        final RequestOptions requestOptions = RequestOptions.builder().withQueryParams(queryParams).build();
+        final AccountTimeline accountTimelineInParallel = killBillClient.getAccountTimeline(accountId, auditLevel, requestOptions);
+        Assert.assertEquals(accountTimelineInParallel, accountTimeline);
+
+        return accountTimeline;
+    }
 }
diff --git a/profiles/killpay/pom.xml b/profiles/killpay/pom.xml
index d08edaf..87a8077 100644
--- a/profiles/killpay/pom.xml
+++ b/profiles/killpay/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <artifactId>killbill-profiles</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles-killpay</artifactId>

profiles/pom.xml 2(+1 -1)

diff --git a/profiles/pom.xml b/profiles/pom.xml
index 7978092..22c7a33 100644
--- a/profiles/pom.xml
+++ b/profiles/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles</artifactId>
diff --git a/subscription/pom.xml b/subscription/pom.xml
index 111ed11..77e04bb 100644
--- a/subscription/pom.xml
+++ b/subscription/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-subscription</artifactId>

tenant/pom.xml 2(+1 -1)

diff --git a/tenant/pom.xml b/tenant/pom.xml
index bfc6e8a..69fe1bf 100644
--- a/tenant/pom.xml
+++ b/tenant/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-tenant</artifactId>

usage/pom.xml 2(+1 -1)

diff --git a/usage/pom.xml b/usage/pom.xml
index 44755bc..0b98838 100644
--- a/usage/pom.xml
+++ b/usage/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-usage</artifactId>

util/pom.xml 2(+1 -1)

diff --git a/util/pom.xml b/util/pom.xml
index 6aa221c..80b2a42 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.7-SNAPSHOT</version>
+        <version>0.17.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-util</artifactId>
diff --git a/util/src/main/java/org/killbill/billing/util/export/dao/CSVExportOutputStream.java b/util/src/main/java/org/killbill/billing/util/export/dao/CSVExportOutputStream.java
index 2b0d14c..154415e 100644
--- a/util/src/main/java/org/killbill/billing/util/export/dao/CSVExportOutputStream.java
+++ b/util/src/main/java/org/killbill/billing/util/export/dao/CSVExportOutputStream.java
@@ -63,6 +63,9 @@ public class CSVExportOutputStream extends OutputStream implements DatabaseExpor
         currentTableName = tableName;
 
         final CsvSchema.Builder builder = CsvSchema.builder();
+        // Remove quoting of character which applies (somewhat arbitrarily, Tatu???) for string whose length is greater than MAX_QUOTE_CHECK = 24 -- See CVSWriter#_mayNeedQuotes
+        builder.disableQuoteChar();
+
         for (final ColumnInfo columnInfo : columnsForTable) {
             builder.addColumn(columnInfo.getColumnName(), getColumnTypeFromSqlType(columnInfo.getDataType()));
         }
diff --git a/util/src/main/java/org/killbill/billing/util/export/dao/DatabaseExportDao.java b/util/src/main/java/org/killbill/billing/util/export/dao/DatabaseExportDao.java
index 5fe8093..1f37627 100644
--- a/util/src/main/java/org/killbill/billing/util/export/dao/DatabaseExportDao.java
+++ b/util/src/main/java/org/killbill/billing/util/export/dao/DatabaseExportDao.java
@@ -50,6 +50,8 @@ public class DatabaseExportDao {
     private enum TableType {
         /* TableName.ACCOUNT */
         KB_ACCOUNT("record_id", "tenant_record_id"),
+        /* TableName.ACCOUNT_HISTORY */
+        KB_ACCOUNT_HISTORY("target_record_id", "tenant_record_id"),
         /* Any per-account data table */
         KB_PER_ACCOUNT("account_record_id", "tenant_record_id"),
         /* bus_events, notifications table */
@@ -102,8 +104,15 @@ public class DatabaseExportDao {
     private void exportDataForAccountAndTable(final DatabaseExportOutputStream out, final List<ColumnInfo> columnsForTable, final InternalTenantContext context) {
 
 
+        TableType tableType = TableType.OTHER;
         final String tableName = columnsForTable.get(0).getTableName();
-        TableType tableType = TableName.ACCOUNT.getTableName().equals(tableName) ? TableType.KB_ACCOUNT : TableType.OTHER;
+
+        if (TableName.ACCOUNT.getTableName().equals(tableName)) {
+            tableType = TableType.KB_ACCOUNT;
+        } else if (TableName.ACCOUNT_HISTORY.getTableName().equals(tableName)) {
+            tableType = TableType.KB_ACCOUNT_HISTORY;
+        }
+
         boolean firstColumn = true;
         final StringBuilder queryBuilder = new StringBuilder("select ");
         for (final ColumnInfo column : columnsForTable) {
@@ -125,7 +134,7 @@ public class DatabaseExportDao {
         }
 
         // Don't export non-account specific tables
-        if (tableType ==  TableType.OTHER) {
+        if (tableType == TableType.OTHER) {
             return;
         }
 
diff --git a/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java b/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java
index 855320f..a4ffe97 100644
--- a/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java
+++ b/util/src/test/java/org/killbill/billing/util/export/dao/TestDatabaseExportDao.java
@@ -81,12 +81,13 @@ public class TestDatabaseExportDao extends UtilTestSuiteWithEmbeddedDB {
         // Verify new dump
         final String newDump = getDump();
         Assert.assertEquals(newDump, "-- accounts record_id,id,external_key,email,name,first_name_length,currency,billing_cycle_day_local,payment_method_id,time_zone,locale,address1,address2,company_name,city,state_or_province,country,postal_code,phone,migrated,is_notified_for_invoices,created_date,created_by,updated_date,updated_by,tenant_record_id\n" +
-                                     String.format("%s,\"%s\",,%s,%s,%s,,,,,,,,,,,,,,false,%s,\"%s\",%s,\"%s\",%s,%s", internalCallContext.getAccountRecordId(), accountId, accountEmail, accountName, firstNameLength,
+                                     String.format("%s,%s,,%s,%s,%s,,,,,,,,,,,,,,false,%s,%s,%s,%s,%s,%s", internalCallContext.getAccountRecordId(), accountId, accountEmail, accountName, firstNameLength,
                                                    isNotifiedForInvoices, "1970-05-24T18:33:02.000+0000", createdBy, "1982-02-18T20:03:42.000+0000", updatedBy, internalCallContext.getTenantRecordId()) + "\n" +
                                      "-- " + tableNameA + " record_id,a_column,account_record_id,tenant_record_id\n" +
                                      "1,a," + internalCallContext.getAccountRecordId() + "," + internalCallContext.getTenantRecordId() + "\n" +
                                      "-- " + tableNameB + " record_id,b_column,account_record_id,tenant_record_id\n" +
                                      "1,b," + internalCallContext.getAccountRecordId() + "," + internalCallContext.getTenantRecordId() + "\n");
+
     }
 
     private String getDump() {