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);