killbill-memoizeit

invoice: don't delete a CBA if the resulting invoice balance

9/24/2012 3:17:55 PM

Details

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