killbill-memoizeit

invoice: CBA improvements Ignore CBA from DRAFT invoices

2/28/2017 5:00:15 PM

Details

diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
index 230fdd1..d2ce6d3 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/CBADao.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * 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
  * License.  You may obtain a copy of the License at:
  *
@@ -24,13 +26,12 @@ import java.util.UUID;
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 
-import org.killbill.billing.invoice.api.InvoiceApiException;
-import org.killbill.billing.invoice.api.InvoiceItem;
-import org.killbill.billing.invoice.api.InvoiceItemType;
-import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.entity.EntityPersistenceException;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceStatus;
+import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
 import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
 
 import com.google.common.base.Predicate;
@@ -46,41 +47,35 @@ public class CBADao {
         this.invoiceDaoHelper = invoiceDaoHelper;
     }
 
-
-    public BigDecimal getAccountCBAFromTransaction(final UUID accountId,
-                                                    final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
-                                                    final InternalTenantContext context) {
-        final List<InvoiceModelDao> invoices = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
-        return getAccountCBAFromTransaction(invoices);
-    }
-
-    public BigDecimal getAccountCBAFromTransaction(final List<InvoiceModelDao> invoices) {
-        BigDecimal cba = BigDecimal.ZERO;
-        for (final InvoiceModelDao cur : invoices) {
-            cba = cba.add(InvoiceModelDaoHelper.getCBAAmount(cur));
-        }
-        return cba;
+    // PERF: Compute the CBA directly in the database (faster than re-constructing all invoices)
+    public BigDecimal getAccountCBAFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) {
+        final InvoiceItemSqlDao invoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
+        return invoiceItemSqlDao.getAccountCBA(context);
     }
 
     // We expect a clean up to date invoice, with all the items except the cba, that we will compute in that method
-    public InvoiceItemModelDao computeCBAComplexity(final InvoiceModelDao invoice, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
-
+    public InvoiceItemModelDao computeCBAComplexity(final InvoiceModelDao invoice,
+                                                    @Nullable final BigDecimal accountCBAOrNull,
+                                                    @Nullable final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+                                                    final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
         final BigDecimal balance = getInvoiceBalance(invoice);
 
-        // Current balance is negative, we need to generate a credit (positive CBA amount).
         if (balance.compareTo(BigDecimal.ZERO) < 0) {
-            return new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(), balance.negate(), invoice.getCurrency()));
-
-        // Current balance is positive, we need to use some of the existing if available (negative CBA amount)
-        } else if (balance.compareTo(BigDecimal.ZERO) > 0) {
+            // Current balance is negative, we need to generate a credit (positive CBA amount)
+            return buildCBAItem(invoice, balance, context);
+        } else if (balance.compareTo(BigDecimal.ZERO) > 0 && invoice.getStatus() == InvoiceStatus.COMMITTED) {
+            // Current balance is positive and the invoice is COMMITTED, we need to use some of the existing if available (negative CBA amount)
+            // PERF: in some codepaths, the CBA maybe have already been computed
+            BigDecimal accountCBA = accountCBAOrNull;
+            if (accountCBAOrNull == null) {
+                accountCBA = getAccountCBAFromTransaction(entitySqlDaoWrapperFactory, context);
+            }
 
-            final List<InvoiceModelDao> allInvoices = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
-            final BigDecimal accountCBA = getAccountCBAFromTransaction(allInvoices);
             if (accountCBA.compareTo(BigDecimal.ZERO) <= 0) {
                 return null;
             }
             final BigDecimal positiveCreditAmount = accountCBA.compareTo(balance) > 0 ? balance : accountCBA;
-            return new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(), positiveCreditAmount.negate(), invoice.getCurrency()));
+            return buildCBAItem(invoice, positiveCreditAmount, context);
         } else {
             // 0 balance, nothing to do.
             return null;
@@ -113,64 +108,49 @@ public class CBADao {
     }
 
     // We let the code below rehydrate the invoice before we can add the CBA item
-    public void addCBAComplexityFromTransaction(final UUID invoiceId, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
-
+    // PERF: when possible, prefer the method below to avoid re-fetching the invoice
+    public void doCBAComplexityFromTransaction(final UUID invoiceId,
+                                               final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+                                               final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
         final InvoiceSqlDao transInvoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
         final InvoiceModelDao invoice = transInvoiceDao.getById(invoiceId.toString(), context);
         invoiceDaoHelper.populateChildren(invoice, entitySqlDaoWrapperFactory, context);
-        addCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
+
+        doCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
     }
 
-    // We expect a clean up to date invoice, with all the items except the CBA, that we will compute in that method
-    public void addCBAComplexityFromTransaction(final InvoiceModelDao invoice, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
-        final InvoiceItemModelDao cbaItem = computeCBAComplexity(invoice, entitySqlDaoWrapperFactory, context);
-        if (cbaItem != null) {
-            final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
-            transInvoiceItemDao.create(cbaItem, context);
-        }
-        List<InvoiceModelDao> invoiceItemModelDaos = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
-        useExistingCBAFromTransaction(invoiceItemModelDaos, entitySqlDaoWrapperFactory, context);
+    public void doCBAComplexityFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+                                               final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+        doCBAComplexityFromTransaction((InvoiceModelDao) null, entitySqlDaoWrapperFactory, context);
     }
 
-    public void addCBAComplexityFromTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+    // Note! We expect an *up-to-date* invoice, with all the items and payments except the CBA, that we will compute in that method
+    public void doCBAComplexityFromTransaction(@Nullable final InvoiceModelDao invoice,
+                                               final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+                                               final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+        // PERF: It is expensive to retrieve and construct all invoice objects. To check if there is effectively something to use, compute the CBA by the database first
+        BigDecimal remainingAccountCBA = getAccountCBAFromTransaction(entitySqlDaoWrapperFactory, context);
 
-        List<InvoiceModelDao> invoiceItemModelDaos = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
-        for (InvoiceModelDao cur : invoiceItemModelDaos) {
-            addCBAIfNeeded(entitySqlDaoWrapperFactory, cur, context);
+        if (invoice != null) {
+            // Generate or use CBA for that specific invoice
+            remainingAccountCBA = computeCBAComplexityAndCreateCBAItem(remainingAccountCBA, invoice, entitySqlDaoWrapperFactory, context);
         }
-        invoiceItemModelDaos = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
-        useExistingCBAFromTransaction(invoiceItemModelDaos, entitySqlDaoWrapperFactory, context);
-    }
 
-    /**
-     * Adjust the invoice with a CBA item if the new invoice balance is negative.
-     *
-     * @param entitySqlDaoWrapperFactory the EntitySqlDaoWrapperFactory from the current transaction
-     * @param invoice                    the invoice to adjust
-     * @param context                    the call callcontext
-     */
-    private void addCBAIfNeeded(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
-                                final InvoiceModelDao invoice,
-                                final InternalCallContext context) throws EntityPersistenceException {
-
-        // If invoice balance becomes negative we add some CBA item
-        final BigDecimal balance = InvoiceModelDaoHelper.getBalance(invoice);
-        if (balance.compareTo(BigDecimal.ZERO) < 0) {
-            final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
-            final InvoiceItemModelDao cbaAdjItem = new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(), balance.negate(), invoice.getCurrency()));
-            transInvoiceItemDao.create(cbaAdjItem, context);
-        }
+        useExistingCBAFromTransaction(remainingAccountCBA, entitySqlDaoWrapperFactory, context);
     }
 
-
-    private void useExistingCBAFromTransaction(final List<InvoiceModelDao> invoices, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalCallContext context) throws InvoiceApiException, EntityPersistenceException {
-
-        final BigDecimal accountCBA = getAccountCBAFromTransaction(invoices);
+    // Distribute account CBA across all COMMITTED unpaid invoices
+    private void useExistingCBAFromTransaction(final BigDecimal accountCBA,
+                                               final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+                                               final InternalCallContext context) throws InvoiceApiException, EntityPersistenceException {
         if (accountCBA.compareTo(BigDecimal.ZERO) <= 0) {
             return;
         }
 
-        final List<InvoiceModelDao> unpaidInvoices = invoiceDaoHelper.getUnpaidInvoicesByAccountFromTransaction(invoices, null);
+        // PERF: Computing the invoice balance is difficult to do in the DB, so we effectively need to retrieve all invoices on the account and filter the unpaid ones in memory.
+        // This should be infrequent though because of the account CBA check above.
+        final List<InvoiceModelDao> allInvoices = invoiceDaoHelper.getAllInvoicesByAccountFromTransaction(entitySqlDaoWrapperFactory, context);
+        final List<InvoiceModelDao> unpaidInvoices = invoiceDaoHelper.getUnpaidInvoicesByAccountFromTransaction(allInvoices, null);
         // We order the same os BillingStateCalculator-- should really share the comparator
         final List<InvoiceModelDao> orderedUnpaidInvoices = Ordering.from(new Comparator<InvoiceModelDao>() {
             @Override
@@ -180,20 +160,46 @@ public class CBADao {
         }).immutableSortedCopy(unpaidInvoices);
 
         BigDecimal remainingAccountCBA = accountCBA;
-        for (InvoiceModelDao cur : orderedUnpaidInvoices) {
-            final BigDecimal curInvoiceBalance = InvoiceModelDaoHelper.getBalance(cur);
-            final BigDecimal cbaToApplyOnInvoice = remainingAccountCBA.compareTo(curInvoiceBalance) <= 0 ? remainingAccountCBA : curInvoiceBalance;
-            remainingAccountCBA = remainingAccountCBA.subtract(cbaToApplyOnInvoice);
-
-            final InvoiceItemModelDao cbaAdjItem = new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(cur.getId(), cur.getAccountId(), context.getCreatedDate().toLocalDate(), cbaToApplyOnInvoice.negate(), cur.getCurrency()));
-
-            final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
-            transInvoiceItemDao.create(cbaAdjItem, context);
-
+        for (final InvoiceModelDao unpaidInvoice : orderedUnpaidInvoices) {
+            remainingAccountCBA = computeCBAComplexityAndCreateCBAItem(remainingAccountCBA, unpaidInvoice, entitySqlDaoWrapperFactory, context);
             if (remainingAccountCBA.compareTo(BigDecimal.ZERO) <= 0) {
                 break;
             }
         }
     }
 
+    // Return the updated account CBA
+    private BigDecimal computeCBAComplexityAndCreateCBAItem(final BigDecimal accountCBA,
+                                                            final InvoiceModelDao invoice,
+                                                            final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+                                                            final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+        final InvoiceItemModelDao cbaItem = computeCBAComplexity(invoice, accountCBA, entitySqlDaoWrapperFactory, context);
+        if (cbaItem != null) {
+            createCBAItem(invoice, cbaItem, entitySqlDaoWrapperFactory, context);
+            return accountCBA.add(cbaItem.getAmount());
+        } else {
+            return accountCBA;
+        }
+    }
+
+    private void createCBAItem(final InvoiceModelDao invoiceModelDao,
+                               final InvoiceItemModelDao cbaItem,
+                               final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory,
+                               final InternalCallContext context) throws EntityPersistenceException {
+        final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
+        transInvoiceItemDao.create(cbaItem, context);
+
+        // Refresh the in-memory item
+        invoiceModelDao.addInvoiceItem(cbaItem);
+    }
+
+    private InvoiceItemModelDao buildCBAItem(final InvoiceModelDao invoice,
+                                             final BigDecimal amount,
+                                             final InternalCallContext context) {
+        return new InvoiceItemModelDao(new CreditBalanceAdjInvoiceItem(invoice.getId(),
+                                                                       invoice.getAccountId(),
+                                                                       context.getCreatedDate().toLocalDate(),
+                                                                       amount.negate(),
+                                                                       invoice.getCurrency()));
+    }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 4d64d57..02e405e 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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
@@ -278,7 +278,9 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                     for (final InvoiceItemModelDao invoiceItemModelDao : invoiceItems) {
                         createInvoiceItemFromTransaction(transInvoiceItemSqlDao, invoiceItemModelDao, context);
                     }
-                    cbaDao.addCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
+                    // Keep invoice up-to-date for CBA below
+                    invoice.addInvoiceItems(invoiceItems);
+                    cbaDao.doCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
                     if (InvoiceStatus.COMMITTED.equals(invoice.getStatus())) {
                         notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), callbackDateTimePerSubscriptions, context);
                     }
@@ -320,8 +322,13 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                         }
                     }
 
-                    if (madeChanges) {
-                        cbaDao.addCBAComplexityFromTransaction(invoiceModelDao.getId(), entitySqlDaoWrapperFactory, context);
+                    if (newInvoice) {
+                        // New invoice, so no associated payment yet: no need to refresh the invoice state
+                        cbaDao.doCBAComplexityFromTransaction(invoiceModelDao, entitySqlDaoWrapperFactory, context);
+                    } else if (madeChanges) {
+                        // Existing invoice (e.g. we're processing an adjustment): refresh the invoice state to get the correct balance
+                        // Should we maybe enforce callers (e.g. InvoiceApiHelper) to properly populate these invoices?
+                        cbaDao.doCBAComplexityFromTransaction(invoiceModelDao.getId(), entitySqlDaoWrapperFactory, context);
                     }
 
                     if (InvoiceStatus.COMMITTED.equals(invoiceModelDao.getStatus())) {
@@ -411,7 +418,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<BigDecimal>() {
             @Override
             public BigDecimal inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
-                return cbaDao.getAccountCBAFromTransaction(accountId, entitySqlDaoWrapperFactory, context);
+                return cbaDao.getAccountCBAFromTransaction(entitySqlDaoWrapperFactory, context);
             }
         });
     }
@@ -530,7 +537,8 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                     }
                 }
 
-                cbaDao.addCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
+                // The invoice object has been kept up-to-date
+                cbaDao.doCBAComplexityFromTransaction(invoice, entitySqlDaoWrapperFactory, context);
 
                 if (isInvoiceAdjusted) {
                     notifyBusOfInvoiceAdjustment(entitySqlDaoWrapperFactory, invoice.getId(), invoice.getAccountId(), context.getUserToken(), context);
@@ -586,7 +594,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                 // Notify the bus since the balance of the invoice changed
                 final UUID accountId = transactional.getAccountIdFromInvoicePaymentId(chargeBack.getId().toString(), context);
 
-                cbaDao.addCBAComplexityFromTransaction(payment.getInvoiceId(), entitySqlDaoWrapperFactory, context);
+                cbaDao.doCBAComplexityFromTransaction(payment.getInvoiceId(), entitySqlDaoWrapperFactory, context);
 
                 notifyBusOfInvoicePayment(entitySqlDaoWrapperFactory, chargeBack, accountId, context.getUserToken(), context);
 
@@ -622,7 +630,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                 // Notify the bus since the balance of the invoice changed
                 final UUID accountId = transactional.getAccountIdFromInvoicePaymentId(chargebackReversed.getId().toString(), context);
 
-                cbaDao.addCBAComplexityFromTransaction(chargebackReversed.getInvoiceId(), entitySqlDaoWrapperFactory, context);
+                cbaDao.doCBAComplexityFromTransaction(chargebackReversed.getInvoiceId(), entitySqlDaoWrapperFactory, context);
 
                 notifyBusOfInvoicePayment(entitySqlDaoWrapperFactory, chargebackReversed, accountId, context.getUserToken(), context);
 
@@ -636,7 +644,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
         return transactionalSqlDao.execute(InvoiceApiException.class, new EntitySqlDaoTransactionWrapper<InvoiceItemModelDao>() {
             @Override
             public InvoiceItemModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
-                final InvoiceItemModelDao cbaNewItem = cbaDao.computeCBAComplexity(invoice, entitySqlDaoWrapperFactory, context);
+                final InvoiceItemModelDao cbaNewItem = cbaDao.computeCBAComplexity(invoice, null, entitySqlDaoWrapperFactory, context);
                 return cbaNewItem;
             }
         });
@@ -836,7 +844,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
 
                 // If there is more account credit than CBA we adjusted, we're done.
                 // Otherwise, we need to find further invoices on which this credit was consumed
-                final BigDecimal accountCBA = cbaDao.getAccountCBAFromTransaction(accountId, entitySqlDaoWrapperFactory, context);
+                final BigDecimal accountCBA = cbaDao.getAccountCBAFromTransaction(entitySqlDaoWrapperFactory, context);
                 if (accountCBA.compareTo(BigDecimal.ZERO) < 0) {
                     if (accountCBA.compareTo(cbaItem.getAmount().negate()) < 0) {
                         throw new IllegalStateException("The account balance can't be lower than the amount adjusted");
@@ -895,12 +903,12 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
         });
     }
 
+    @Override
     public void consumeExstingCBAOnAccountWithUnpaidInvoices(final UUID accountId, final InternalCallContext context) {
         transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
             @Override
             public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
-                // In theory we should only have to call useExistingCBAFromTransaction but just to be safe we also check for credit generation
-                cbaDao.addCBAComplexityFromTransaction(entitySqlDaoWrapperFactory, context);
+                cbaDao.doCBAComplexityFromTransaction(entitySqlDaoWrapperFactory, context);
                 return null;
             }
         });
@@ -1017,6 +1025,8 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
 
                 transactional.updateStatus(invoiceId.toString(), newStatus.toString(), context);
 
+                cbaDao.doCBAComplexityFromTransaction(entitySqlDaoWrapperFactory, context);
+
                 if (InvoiceStatus.COMMITTED.equals(newStatus)) {
                     // notify invoice creation event
                     notifyBusOfInvoiceCreation(entitySqlDaoWrapperFactory, invoice, context);
@@ -1163,19 +1173,25 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
 
 
                 // save invoices and invoice items
-                InvoiceModelDao childInvoice = new InvoiceModelDao(invoiceForExternalCharge);
+                final InvoiceModelDao childInvoice = new InvoiceModelDao(invoiceForExternalCharge);
                 invoiceSqlDao.create(childInvoice, childAccountContext);
-                createInvoiceItemFromTransaction(transInvoiceItemSqlDao, new InvoiceItemModelDao(externalChargeItem), childAccountContext);
+                final InvoiceItemModelDao childExternalChargeItem = new InvoiceItemModelDao(externalChargeItem);
+                createInvoiceItemFromTransaction(transInvoiceItemSqlDao, childExternalChargeItem, childAccountContext);
+                // Keep invoice up-to-date for CBA below
+                childInvoice.addInvoiceItem(childExternalChargeItem);
 
-                InvoiceModelDao parentInvoice = new InvoiceModelDao(invoiceForCredit);
+                final InvoiceModelDao parentInvoice = new InvoiceModelDao(invoiceForCredit);
                 invoiceSqlDao.create(parentInvoice, parentAccountContext);
-                createInvoiceItemFromTransaction(transInvoiceItemSqlDao, new InvoiceItemModelDao(creditItem), parentAccountContext);
+                final InvoiceItemModelDao parentCreditItem = new InvoiceItemModelDao(creditItem);
+                createInvoiceItemFromTransaction(transInvoiceItemSqlDao, parentCreditItem, parentAccountContext);
+                // Keep invoice up-to-date for CBA below
+                parentInvoice.addInvoiceItem(parentCreditItem);
 
                 // add CBA complexity and notify bus on child invoice creation
-                cbaDao.addCBAComplexityFromTransaction(childInvoice.getId(), entitySqlDaoWrapperFactory, childAccountContext);
+                cbaDao.doCBAComplexityFromTransaction(childInvoice, entitySqlDaoWrapperFactory, childAccountContext);
                 notifyBusOfInvoiceCreation(entitySqlDaoWrapperFactory, childInvoice, childAccountContext);
 
-                cbaDao.addCBAComplexityFromTransaction(parentInvoice.getId(), entitySqlDaoWrapperFactory, parentAccountContext);
+                cbaDao.doCBAComplexityFromTransaction(parentInvoice, entitySqlDaoWrapperFactory, parentAccountContext);
                 notifyBusOfInvoiceCreation(entitySqlDaoWrapperFactory, parentInvoice, parentAccountContext);
 
                 return null;
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java
index e30b5d2..8c7ded7 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * 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
  * License.  You may obtain a copy of the License at:
  *
@@ -20,16 +22,15 @@ import java.math.BigDecimal;
 import java.util.List;
 
 import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.util.audit.ChangeType;
 import org.killbill.billing.util.entity.dao.Audited;
+import org.killbill.billing.util.entity.dao.EntitySqlDao;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.BindBean;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
-
-import org.killbill.billing.invoice.api.InvoiceItem;
-import org.killbill.billing.callcontext.InternalTenantContext;
-import org.killbill.billing.util.entity.dao.EntitySqlDao;
-import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 
 @EntitySqlDaoStringTemplate
@@ -57,4 +58,7 @@ public interface InvoiceItemSqlDao extends EntitySqlDao<InvoiceItemModelDao, Inv
     @SqlQuery
     List<InvoiceItemModelDao> getInvoiceItemsByParentInvoice(@Bind("parentInvoiceId") final String parentInvoiceId,
                                                              @BindBean final InternalTenantContext context);
+
+    @SqlQuery
+    BigDecimal getAccountCBA(@BindBean final InternalTenantContext context);
 }
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
index ae19a63..c99d95c 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/dao/InvoiceItemSqlDao.sql.stg
@@ -85,4 +85,17 @@ getInvoiceItemsByParentInvoice() ::= <<
   <AND_CHECK_TENANT("items.")>
   <defaultOrderBy()>
   ;
->>
\ No newline at end of file
+>>
+
+getAccountCBA() ::= <<
+select coalesce(sum(ii.amount), 0) cba
+from invoice_items ii
+join invoices i on i.id = ii.invoice_id
+where i.status = 'COMMITTED'
+and ii.type = 'CBA_ADJ'
+and <accountRecordIdField("i.")> = :accountRecordId
+and <accountRecordIdField("ii.")> = :accountRecordId
+<AND_CHECK_TENANT("i.")>
+<AND_CHECK_TENANT("ii.")>
+;
+>>
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
index 1bf8600..08999ce 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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
@@ -45,8 +45,6 @@ import org.killbill.billing.catalog.api.PhaseType;
 import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanPhase;
 import org.killbill.billing.entity.EntityPersistenceException;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
 import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
 import org.killbill.billing.invoice.MockBillingEventSet;
 import org.killbill.billing.invoice.api.Invoice;
@@ -815,7 +813,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         final UUID accountId = account.getId();
         final UUID bundleId = UUID.randomUUID();
 
-        createCredit(accountId, clock.getUTCToday(), new BigDecimal("20.0"));
+        final InvoiceItemModelDao credit = createCredit(accountId, clock.getUTCToday(), new BigDecimal("20.0"), true);
 
         final String description = UUID.randomUUID().toString();
         final InvoiceModelDao invoiceForExternalCharge = new InvoiceModelDao(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD, false);
@@ -823,8 +821,55 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         invoiceForExternalCharge.addInvoiceItem(externalCharge);
         final InvoiceItemModelDao charge = invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(invoiceForExternalCharge), context).get(0);
 
-        final InvoiceModelDao newInvoice = invoiceDao.getById(charge.getInvoiceId(), context);
-        final List<InvoiceItemModelDao> items = newInvoice.getInvoiceItems();
+        InvoiceModelDao newInvoice = invoiceDao.getById(charge.getInvoiceId(), context);
+        List<InvoiceItemModelDao> items = newInvoice.getInvoiceItems();
+        // No CBA consumed yet since the credit was created on a DRAFT invoice
+        assertEquals(items.size(), 1);
+        assertEquals(items.get(0).getType(), InvoiceItemType.EXTERNAL_CHARGE);
+        assertEquals(items.get(0).getDescription(), description);
+
+        invoiceDao.changeInvoiceStatus(credit.getInvoiceId(), InvoiceStatus.COMMITTED, context);
+
+        // CBA should have been consumed
+        newInvoice = invoiceDao.getById(charge.getInvoiceId(), context);
+        items = newInvoice.getInvoiceItems();
+        assertEquals(items.size(), 2);
+        for (final InvoiceItemModelDao cur : items) {
+            if (cur.getId().equals(charge.getId())) {
+                assertEquals(cur.getType(), InvoiceItemType.EXTERNAL_CHARGE);
+                assertEquals(cur.getDescription(), description);
+            } else {
+                assertEquals(cur.getType(), InvoiceItemType.CBA_ADJ);
+                assertTrue(cur.getAmount().compareTo(new BigDecimal("-15.00")) == 0);
+            }
+        }
+    }
+
+    @Test(groups = "slow")
+    public void testExternalChargeOnDRAFTInvoiceWithCBA() throws InvoiceApiException, EntityPersistenceException {
+        final UUID accountId = account.getId();
+        final UUID bundleId = UUID.randomUUID();
+
+        final InvoiceItemModelDao credit = createCredit(accountId, clock.getUTCToday(), new BigDecimal("20.0"), false);
+
+        final String description = UUID.randomUUID().toString();
+        final InvoiceModelDao draftInvoiceForExternalCharge = new InvoiceModelDao(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD, false, InvoiceStatus.DRAFT);
+        final InvoiceItemModelDao externalCharge = new InvoiceItemModelDao(new ExternalChargeInvoiceItem(draftInvoiceForExternalCharge.getId(), accountId, bundleId, description, clock.getUTCToday(), new BigDecimal("15.0"), Currency.USD));
+        draftInvoiceForExternalCharge.addInvoiceItem(externalCharge);
+        final InvoiceItemModelDao charge = invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(draftInvoiceForExternalCharge), context).get(0);
+
+        InvoiceModelDao newInvoice = invoiceDao.getById(charge.getInvoiceId(), context);
+        List<InvoiceItemModelDao> items = newInvoice.getInvoiceItems();
+        // No CBA consumed yet since the charge was created on a DRAFT invoice
+        assertEquals(items.size(), 1);
+        assertEquals(items.get(0).getType(), InvoiceItemType.EXTERNAL_CHARGE);
+        assertEquals(items.get(0).getDescription(), description);
+
+        invoiceDao.changeInvoiceStatus(charge.getInvoiceId(), InvoiceStatus.COMMITTED, context);
+
+        // CBA should have been consumed
+        newInvoice = invoiceDao.getById(charge.getInvoiceId(), context);
+        items = newInvoice.getInvoiceItems();
         assertEquals(items.size(), 2);
         for (final InvoiceItemModelDao cur : items) {
             if (cur.getId().equals(charge.getId())) {
@@ -926,13 +971,13 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
     }
 
     @Test(groups = "slow")
-    public void testAccountCredit() {
+    public void testAccountCredit() throws InvoiceApiException {
         final UUID accountId = account.getId();
         final LocalDate effectiveDate = new LocalDate(2011, 3, 1);
 
         final BigDecimal creditAmount = new BigDecimal("5.0");
 
-        createCredit(accountId, effectiveDate, creditAmount);
+        createCredit(accountId, effectiveDate, creditAmount, true);
 
         final List<InvoiceModelDao> invoices = invoiceDao.getAllInvoicesByAccount(context);
         assertEquals(invoices.size(), 1);
@@ -954,6 +999,12 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         }
         assertTrue(foundCredit);
         assertTrue(foundCBA);
+
+        // No account CBA yet since the invoice is in DRAFT mode
+        assertEquals(invoiceDao.getAccountCBA(accountId, context).compareTo(BigDecimal.ZERO), 0);
+
+        invoiceDao.changeInvoiceStatus(invoice.getId(), InvoiceStatus.COMMITTED, context);
+        assertEquals(invoiceDao.getAccountCBA(accountId, context).compareTo(creditAmount), 0);
     }
 
     @Test(groups = "slow")
@@ -1001,7 +1052,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         // Create the credit item
         final LocalDate effectiveDate = new LocalDate(2011, 3, 1);
 
-        createCredit(accountId, invoice1.getId(), effectiveDate, creditAmount);
+        createCredit(accountId, invoice1.getId(), effectiveDate, creditAmount, false);
 
         final List<InvoiceModelDao> invoices = invoiceDao.getAllInvoicesByAccount(context);
         assertEquals(invoices.size(), 1);
@@ -1116,7 +1167,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         assertEquals(allInvoicesByAccount.size(), 1);
 
         // insert DRAFT invoice
-        createCredit(accountId, new LocalDate(2011, 12, 31), BigDecimal.TEN);
+        createCredit(accountId, new LocalDate(2011, 12, 31), BigDecimal.TEN, true);
 
         allInvoicesByAccount = invoiceDao.getInvoicesByAccount(new LocalDate(2011, 1, 1), context);
         assertEquals(allInvoicesByAccount.size(), 2);
@@ -1696,14 +1747,14 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
     }
 
 
-    private void createCredit(final UUID accountId, final LocalDate effectiveDate, final BigDecimal creditAmount) {
-        createCredit(accountId, null, effectiveDate, creditAmount);
+    private InvoiceItemModelDao createCredit(final UUID accountId, final LocalDate effectiveDate, final BigDecimal creditAmount, final boolean draft) {
+        return createCredit(accountId, null, effectiveDate, creditAmount, draft);
     }
 
-    private void createCredit(final UUID accountId, @Nullable final UUID invoiceId, final LocalDate effectiveDate, final BigDecimal creditAmount) {
+    private InvoiceItemModelDao createCredit(final UUID accountId, @Nullable final UUID invoiceId, final LocalDate effectiveDate, final BigDecimal creditAmount, final boolean draft) {
         final InvoiceModelDao invoiceModelDao;
         if (invoiceId == null) {
-            invoiceModelDao = new InvoiceModelDao(accountId, effectiveDate, effectiveDate, Currency.USD, false, InvoiceStatus.DRAFT);
+            invoiceModelDao = new InvoiceModelDao(accountId, effectiveDate, effectiveDate, Currency.USD, false, draft ? InvoiceStatus.DRAFT : InvoiceStatus.COMMITTED);
         } else {
             invoiceModelDao = invoiceDao.getById(invoiceId, context);
         }
@@ -1717,7 +1768,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
                                                                           creditAmount.negate(),
                                                                           invoiceModelDao.getCurrency());
         invoiceModelDao.addInvoiceItem(new InvoiceItemModelDao(invoiceItem));
-        invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(invoiceModelDao), context);
+        return invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(invoiceModelDao), context).get(0);
     }
 
     @Test(groups = "slow")
@@ -1749,9 +1800,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
         InvoiceItem parentInvoiceItem = new ParentInvoiceItem(UUID.randomUUID(), today, parentInvoice.getId(), parentAccountId, childAccountId, BigDecimal.TEN, account.getCurrency(), "");
         parentInvoice.addInvoiceItem(new InvoiceItemModelDao(parentInvoiceItem));
 
-        // build account date time zone
-        final FutureAccountNotifications futureAccountNotifications = new FutureAccountNotifications(ImmutableMap.<UUID, List<SubscriptionNotification>>of());
-        invoiceDao.createInvoice(parentInvoice, parentInvoice.getInvoiceItems(), true, futureAccountNotifications, context);
+        invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(parentInvoice), context);
 
         final InvoiceModelDao parentDraftInvoice = invoiceDao.getParentDraftInvoice(parentAccountId, context);
 
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
index d70c150..9ee931b 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoice.java
@@ -776,7 +776,7 @@ public class TestInvoice extends TestJaxrsBase {
         credit.setCreditAmount(creditAmount);
 
         // insert credit to child account
-        final Credit creditJson = killBillClient.createCredit(credit, false, createdBy, reason, comment);
+        final Credit creditJson = killBillClient.createCredit(credit, true, requestOptions);
 
         Invoices childInvoices = killBillClient.getInvoicesForAccount(childAccount.getAccountId(), true, false);
         Assert.assertEquals(childInvoices.size(), 1);