diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java
index b03626e..a048f24 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/AuditedInvoiceDao.java
@@ -35,12 +35,6 @@ import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Objects;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableMap.Builder;
-import com.google.inject.Inject;
import com.ning.billing.ErrorCode;
import com.ning.billing.catalog.api.Currency;
import com.ning.billing.invoice.api.Invoice;
@@ -72,6 +66,13 @@ import com.ning.billing.util.dao.ObjectType;
import com.ning.billing.util.dao.TableName;
import com.ning.billing.util.tag.ControlTagType;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.inject.Inject;
+
public class AuditedInvoiceDao implements InvoiceDao {
private static final Logger log = LoggerFactory.getLogger(AuditedInvoiceDao.class);
@@ -588,7 +589,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
@Override
public InvoiceItem insertExternalCharge(final UUID accountId, @Nullable final UUID invoiceId, @Nullable final UUID bundleId, final String description,
final BigDecimal amount, final LocalDate effectiveDate, final Currency currency, final CallContext context)
- throws InvoiceApiException {
+ throws InvoiceApiException {
return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
@Override
public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
@@ -682,6 +683,89 @@ public class AuditedInvoiceDao implements InvoiceDao {
}
@Override
+ public void deleteCBA(final UUID accountId, final UUID invoiceId, final UUID invoiceItemId, final CallContext context) throws InvoiceApiException {
+ invoiceSqlDao.inTransaction(new Transaction<Void, InvoiceSqlDao>() {
+ @Override
+ public Void inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+ // Retrieve the invoice and make sure it belongs to the right account
+ final Invoice invoice = transactional.getById(invoiceId.toString());
+ if (invoice == null || !invoice.getAccountId().equals(accountId)) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_NOT_FOUND, invoiceId);
+ }
+
+ // Retrieve the invoice item and make sure it belongs to the right invoice
+ final InvoiceItemSqlDao invoiceItemSqlDao = transactional.become(InvoiceItemSqlDao.class);
+ final InvoiceItem cbaItem = invoiceItemSqlDao.getById(invoiceItemId.toString());
+ if (cbaItem == null || !cbaItem.getInvoiceId().equals(invoice.getId())) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, invoiceItemId);
+ }
+
+ // First, adjust the same invoice with the CBA amount to "delete"
+ final InvoiceItem cbaAdjItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
+ cbaItem.getAmount().negate(), cbaItem.getCurrency());
+ invoiceItemSqlDao.create(cbaAdjItem, context);
+
+ // 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 = getAccountCBAFromTransaction(accountId, invoiceSqlDao);
+ 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");
+ }
+ final List<Invoice> invoicesFollowing = transactional.getInvoicesByAccountAfterDate(accountId.toString(),
+ invoice.getInvoiceDate().toDateTimeAtStartOfDay().toDate());
+ populateChildren(invoicesFollowing, transactional);
+
+ // The remaining amount to adjust (i.e. the amount of credits used on following invoices)
+ // is the current account CBA balance (minus the sign)
+ BigDecimal positiveRemainderToAdjust = accountCBA.negate();
+ for (final Invoice invoiceFollowing : invoicesFollowing) {
+ if (invoiceFollowing.getId().equals(invoice.getId())) {
+ continue;
+ }
+
+ // Add a single adjustment per invoice
+ BigDecimal positiveCBAAdjItemAmount = BigDecimal.ZERO;
+
+ for (final InvoiceItem cbaUsed : invoiceFollowing.getInvoiceItems()) {
+ // Ignore non CBA items or credits (CBA >= 0)
+ if (!InvoiceItemType.CBA_ADJ.equals(cbaUsed.getInvoiceItemType()) ||
+ cbaUsed.getAmount().compareTo(BigDecimal.ZERO) >= 0) {
+ continue;
+ }
+
+ final BigDecimal positiveCBAUsedAmount = cbaUsed.getAmount().negate();
+ final BigDecimal positiveNextCBAAdjItemAmount;
+ if (positiveCBAUsedAmount.compareTo(positiveRemainderToAdjust) < 0) {
+ positiveNextCBAAdjItemAmount = positiveCBAUsedAmount;
+ positiveRemainderToAdjust = positiveRemainderToAdjust.min(positiveNextCBAAdjItemAmount);
+ } else {
+ positiveNextCBAAdjItemAmount = positiveRemainderToAdjust;
+ positiveRemainderToAdjust = BigDecimal.ZERO;
+ }
+ positiveCBAAdjItemAmount = positiveCBAAdjItemAmount.add(positiveNextCBAAdjItemAmount);
+
+ if (positiveRemainderToAdjust.compareTo(BigDecimal.ZERO) == 0) {
+ break;
+ }
+ }
+
+ // Add the adjustment on that invoice
+ final InvoiceItem nextCBAAdjItem = new CreditBalanceAdjInvoiceItem(invoiceFollowing.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
+ positiveCBAAdjItemAmount, cbaItem.getCurrency());
+ invoiceItemSqlDao.create(nextCBAAdjItem, context);
+ if (positiveRemainderToAdjust.compareTo(BigDecimal.ZERO) == 0) {
+ break;
+ }
+ }
+ }
+
+ return null;
+ }
+ });
+ }
+
+ @Override
public void test() {
invoiceSqlDao.test();
}
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
index f7ba009..8e1a32a 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestInvoiceDao.java
@@ -1322,4 +1322,169 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
tags = tagDao.loadEntities(invoice.getId(), ObjectType.INVOICE);
assertEquals(tags.size(), 0);
}
+
+ @Test(groups = "slow")
+ public void testDeleteCBANotConsumed() throws Exception {
+ final UUID accountId = UUID.randomUUID();
+
+ // Create invoice 1
+ // Scenario: single item with payment
+ // * $10 item
+ // Then, a repair occur:
+ // * $-10 repair
+ // * $10 generated CBA due to the repair (assume previous payment)
+ final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceItem fixedItem1 = new FixedPriceInvoiceItem(invoice1.getId(), invoice1.getAccountId(), null, null, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), clock.getUTCToday(), BigDecimal.TEN, Currency.USD);
+ final RepairAdjInvoiceItem repairAdjInvoiceItem = new RepairAdjInvoiceItem(fixedItem1.getInvoiceId(), fixedItem1.getAccountId(),
+ fixedItem1.getStartDate(), fixedItem1.getEndDate(),
+ fixedItem1.getAmount().negate(), fixedItem1.getCurrency(),
+ fixedItem1.getId());
+ final CreditBalanceAdjInvoiceItem creditBalanceAdjInvoiceItem1 = new CreditBalanceAdjInvoiceItem(fixedItem1.getInvoiceId(), fixedItem1.getAccountId(),
+ fixedItem1.getStartDate(), fixedItem1.getAmount(),
+ fixedItem1.getCurrency());
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
+ invoiceItemSqlDao.create(fixedItem1, context);
+ invoiceItemSqlDao.create(repairAdjInvoiceItem, context);
+ invoiceItemSqlDao.create(creditBalanceAdjInvoiceItem1, context);
+
+ // Verify scenario - no CBA should have been used
+ Assert.assertEquals(invoiceDao.getAccountCBA(accountId).doubleValue(), 10.00);
+ verifyInvoice(invoice1.getId(), 10.00, 10.00);
+
+ // Delete the CBA on invoice 1
+ invoiceDao.deleteCBA(accountId, invoice1.getId(), creditBalanceAdjInvoiceItem1.getId(), context);
+
+ // Verify the result
+ Assert.assertEquals(invoiceDao.getAccountCBA(accountId).doubleValue(), 0.00);
+ verifyInvoice(invoice1.getId(), 0.00, 0.00);
+ }
+
+ @Test(groups = "slow")
+ public void testDeleteCBAPartiallyConsumed() throws Exception {
+ final UUID accountId = UUID.randomUUID();
+
+ // Create invoice 1
+ // Scenario: single item with payment
+ // * $10 item
+ // Then, a repair occur:
+ // * $-10 repair
+ // * $10 generated CBA due to the repair (assume previous payment)
+ final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceItem fixedItem1 = new FixedPriceInvoiceItem(invoice1.getId(), invoice1.getAccountId(), null, null, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), clock.getUTCToday(), BigDecimal.TEN, Currency.USD);
+ final RepairAdjInvoiceItem repairAdjInvoiceItem = new RepairAdjInvoiceItem(fixedItem1.getInvoiceId(), fixedItem1.getAccountId(),
+ fixedItem1.getStartDate(), fixedItem1.getEndDate(),
+ fixedItem1.getAmount().negate(), fixedItem1.getCurrency(),
+ fixedItem1.getId());
+ final CreditBalanceAdjInvoiceItem creditBalanceAdjInvoiceItem1 = new CreditBalanceAdjInvoiceItem(fixedItem1.getInvoiceId(), fixedItem1.getAccountId(),
+ fixedItem1.getStartDate(), fixedItem1.getAmount(),
+ fixedItem1.getCurrency());
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
+ invoiceItemSqlDao.create(fixedItem1, context);
+ invoiceItemSqlDao.create(repairAdjInvoiceItem, context);
+ invoiceItemSqlDao.create(creditBalanceAdjInvoiceItem1, context);
+
+ // Create invoice 2
+ // Scenario: single item
+ // * $5 item
+ // * $-5 CBA used
+ final DefaultInvoice invoice2 = new DefaultInvoice(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceItem fixedItem2 = new FixedPriceInvoiceItem(invoice2.getId(), invoice1.getAccountId(), null, null, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), clock.getUTCToday(), new BigDecimal("5"), Currency.USD);
+ final CreditBalanceAdjInvoiceItem creditBalanceAdjInvoiceItem2 = new CreditBalanceAdjInvoiceItem(fixedItem2.getInvoiceId(), fixedItem2.getAccountId(),
+ fixedItem2.getStartDate(), fixedItem2.getAmount().negate(),
+ fixedItem2.getCurrency());
+ invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), true, context);
+ invoiceItemSqlDao.create(fixedItem2, context);
+ invoiceItemSqlDao.create(creditBalanceAdjInvoiceItem2, context);
+
+ // Verify scenario - half of the CBA should have been used
+ Assert.assertEquals(invoiceDao.getAccountCBA(accountId).doubleValue(), 5.00);
+ verifyInvoice(invoice1.getId(), 10.00, 10.00);
+ verifyInvoice(invoice2.getId(), 0.00, -5.00);
+
+ // Delete the CBA on invoice 1
+ invoiceDao.deleteCBA(accountId, invoice1.getId(), creditBalanceAdjInvoiceItem1.getId(), context);
+
+ // Verify all three invoices were affected
+ Assert.assertEquals(invoiceDao.getAccountCBA(accountId).doubleValue(), 0.00);
+ verifyInvoice(invoice1.getId(), 0.00, 0.00);
+ verifyInvoice(invoice2.getId(), 5.00, 0.00);
+ }
+
+ @Test(groups = "slow")
+ public void testDeleteCBAFullyConsumedTwice() throws Exception {
+ final UUID accountId = UUID.randomUUID();
+
+ // Create invoice 1
+ // Scenario: single item with payment
+ // * $10 item
+ // Then, a repair occur:
+ // * $-10 repair
+ // * $10 generated CBA due to the repair (assume previous payment)
+ final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceItem fixedItem1 = new FixedPriceInvoiceItem(invoice1.getId(), invoice1.getAccountId(), null, null, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), clock.getUTCToday(), BigDecimal.TEN, Currency.USD);
+ final RepairAdjInvoiceItem repairAdjInvoiceItem = new RepairAdjInvoiceItem(fixedItem1.getInvoiceId(), fixedItem1.getAccountId(),
+ fixedItem1.getStartDate(), fixedItem1.getEndDate(),
+ fixedItem1.getAmount().negate(), fixedItem1.getCurrency(),
+ fixedItem1.getId());
+ final CreditBalanceAdjInvoiceItem creditBalanceAdjInvoiceItem1 = new CreditBalanceAdjInvoiceItem(fixedItem1.getInvoiceId(), fixedItem1.getAccountId(),
+ fixedItem1.getStartDate(), fixedItem1.getAmount(),
+ fixedItem1.getCurrency());
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
+ invoiceItemSqlDao.create(fixedItem1, context);
+ invoiceItemSqlDao.create(repairAdjInvoiceItem, context);
+ invoiceItemSqlDao.create(creditBalanceAdjInvoiceItem1, context);
+
+ // Create invoice 2
+ // Scenario: single item
+ // * $5 item
+ // * $-5 CBA used
+ final DefaultInvoice invoice2 = new DefaultInvoice(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceItem fixedItem2 = new FixedPriceInvoiceItem(invoice2.getId(), invoice1.getAccountId(), null, null, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), clock.getUTCToday(), new BigDecimal("5"), Currency.USD);
+ final CreditBalanceAdjInvoiceItem creditBalanceAdjInvoiceItem2 = new CreditBalanceAdjInvoiceItem(fixedItem2.getInvoiceId(), fixedItem2.getAccountId(),
+ fixedItem2.getStartDate(), fixedItem2.getAmount().negate(),
+ fixedItem2.getCurrency());
+ invoiceDao.create(invoice2, invoice2.getTargetDate().getDayOfMonth(), true, context);
+ invoiceItemSqlDao.create(fixedItem2, context);
+ invoiceItemSqlDao.create(creditBalanceAdjInvoiceItem2, context);
+
+ // Create invoice 3
+ // Scenario: single item
+ // * $5 item
+ // * $-5 CBA used
+ final DefaultInvoice invoice3 = new DefaultInvoice(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceItem fixedItem3 = new FixedPriceInvoiceItem(invoice3.getId(), invoice1.getAccountId(), null, null, UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), clock.getUTCToday(), new BigDecimal("5"), Currency.USD);
+ final CreditBalanceAdjInvoiceItem creditBalanceAdjInvoiceItem3 = new CreditBalanceAdjInvoiceItem(fixedItem3.getInvoiceId(), fixedItem3.getAccountId(),
+ fixedItem3.getStartDate(), fixedItem3.getAmount().negate(),
+ fixedItem3.getCurrency());
+ invoiceDao.create(invoice3, invoice3.getTargetDate().getDayOfMonth(), true, context);
+ invoiceItemSqlDao.create(fixedItem3, context);
+ invoiceItemSqlDao.create(creditBalanceAdjInvoiceItem3, context);
+
+ // Verify scenario - all CBA should have been used
+ Assert.assertEquals(invoiceDao.getAccountCBA(accountId).doubleValue(), 0.00);
+ verifyInvoice(invoice1.getId(), 10.00, 10.00);
+ verifyInvoice(invoice2.getId(), 0.00, -5.00);
+ verifyInvoice(invoice3.getId(), 0.00, -5.00);
+
+ // Delete the CBA on invoice 1
+ invoiceDao.deleteCBA(accountId, invoice1.getId(), creditBalanceAdjInvoiceItem1.getId(), context);
+
+ // Verify all three invoices were affected
+ Assert.assertEquals(invoiceDao.getAccountCBA(accountId).doubleValue(), 0.00);
+ verifyInvoice(invoice1.getId(), 0.00, 0.00);
+ verifyInvoice(invoice2.getId(), 5.00, 0.00);
+ verifyInvoice(invoice3.getId(), 5.00, 0.00);
+ }
+
+ private void verifyInvoice(final UUID invoiceId, final double balance, final double cbaAmount) throws InvoiceApiException {
+ final Invoice invoice = invoiceDao.getById(invoiceId);
+ Assert.assertEquals(invoice.getBalance().doubleValue(), balance);
+ Assert.assertEquals(invoice.getCBAAmount().doubleValue(), cbaAmount);
+ }
}