diff --git a/api/src/main/java/com/ning/billing/ErrorCode.java b/api/src/main/java/com/ning/billing/ErrorCode.java
index e6d853c..12982f5 100644
--- a/api/src/main/java/com/ning/billing/ErrorCode.java
+++ b/api/src/main/java/com/ning/billing/ErrorCode.java
@@ -193,6 +193,7 @@ public enum ErrorCode {
INVOICE_INVALID_FOR_INVOICE_ITEM_ADJUSTMENT(4012, "Invoice item %s doesn't belong to invoice %s."),
INVOICE_NO_SUCH_EXTERNAL_CHARGE(4014, "External charge item for id %s does not exist"),
EXTERNAL_CHARGE_AMOUNT_INVALID(4015, "External charge amount %s should be strictly positive"),
+ INVOICE_WOULD_BE_NEGATIVE(4016, "Cannot execute operation, the invoice balance would become negative"),
/*
*
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 08db235..dd58f0a 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
@@ -697,6 +697,12 @@ public class AuditedInvoiceDao implements InvoiceDao {
cbaItem.getId(), cbaItem.getAmount().negate(), cbaItem.getCurrency());
invoiceItemSqlDao.create(cbaAdjItem, context);
+ // Verify the final invoice balance is not negative
+ populateChildren(invoice, transactional);
+ if (invoice.getBalance().compareTo(BigDecimal.ZERO) < 0) {
+ throw new InvoiceApiException(ErrorCode.INVOICE_WOULD_BE_NEGATIVE);
+ }
+
// 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);
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 54fce43..b1eaafb 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
@@ -33,6 +33,7 @@ import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.mockito.Mockito;
+import org.skife.jdbi.v2.exceptions.TransactionFailedException;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -1479,6 +1480,44 @@ public class TestInvoiceDao extends InvoiceDaoTestBase {
verifyInvoice(invoice3.getId(), 5.00, 0.00);
}
+ @Test(groups = "slow")
+ public void testCantDeleteCBAIfInvoiceBalanceBecomesNegative() throws Exception {
+ final UUID accountId = UUID.randomUUID();
+
+ // Create invoice 1
+ // Scenario:
+ // * $-10 repair
+ // * $10 generated CBA
+ final Invoice invoice1 = new DefaultInvoice(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final RepairAdjInvoiceItem repairAdjInvoiceItem = new RepairAdjInvoiceItem(invoice1.getId(), invoice1.getAccountId(),
+ invoice1.getInvoiceDate(), invoice1.getInvoiceDate(),
+ BigDecimal.TEN.negate(), invoice1.getCurrency(),
+ UUID.randomUUID());
+ final CreditBalanceAdjInvoiceItem creditBalanceAdjInvoiceItem1 = new CreditBalanceAdjInvoiceItem(invoice1.getId(), invoice1.getAccountId(),
+ invoice1.getInvoiceDate(), repairAdjInvoiceItem.getAmount().negate(),
+ invoice1.getCurrency());
+ invoiceDao.create(invoice1, invoice1.getTargetDate().getDayOfMonth(), true, context);
+ invoiceItemSqlDao.create(repairAdjInvoiceItem, context);
+ invoiceItemSqlDao.create(creditBalanceAdjInvoiceItem1, context);
+
+ // Verify scenario
+ Assert.assertEquals(invoiceDao.getAccountCBA(accountId).doubleValue(), 10.00);
+ verifyInvoice(invoice1.getId(), 0.00, 10.00);
+
+ // Delete the CBA on invoice 1
+ try {
+ invoiceDao.deleteCBA(accountId, invoice1.getId(), creditBalanceAdjInvoiceItem1.getId(), context);
+ Assert.fail();
+ } catch (TransactionFailedException e) {
+ Assert.assertTrue(e.getCause() instanceof InvoiceApiException);
+ Assert.assertEquals(((InvoiceApiException) e.getCause()).getCode(), ErrorCode.INVOICE_WOULD_BE_NEGATIVE.getCode());
+ }
+
+ // Verify the result
+ Assert.assertEquals(invoiceDao.getAccountCBA(accountId).doubleValue(), 10.00);
+ verifyInvoice(invoice1.getId(), 0.00, 10.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);