killbill-memoizeit

Changes

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/api/src/main/java/com/ning/billing/overdue/EmailNotification.java b/api/src/main/java/com/ning/billing/overdue/EmailNotification.java
new file mode 100644
index 0000000..a8e4bf5
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/overdue/EmailNotification.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning 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:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.overdue;
+
+public interface EmailNotification {
+
+    public String getSubject();
+
+    public String getTemplateName();
+
+    public Boolean isHTML();
+}
diff --git a/api/src/main/java/com/ning/billing/overdue/OverdueState.java b/api/src/main/java/com/ning/billing/overdue/OverdueState.java
index 0c216b1..897f6db 100644
--- a/api/src/main/java/com/ning/billing/overdue/OverdueState.java
+++ b/api/src/main/java/com/ning/billing/overdue/OverdueState.java
@@ -20,7 +20,6 @@ import org.joda.time.Period;
 
 import com.ning.billing.junction.api.Blockable;
 
-
 public interface OverdueState<T extends Blockable> {
 
     public String getName();
@@ -40,4 +39,6 @@ public interface OverdueState<T extends Blockable> {
     public Period getReevaluationInterval() throws OverdueApiException;
 
     public Condition<T> getCondition();
+
+    public EmailNotification getEnterStateEmailNotification();
 }
diff --git a/api/src/main/java/com/ning/billing/util/email/EmailSender.java b/api/src/main/java/com/ning/billing/util/email/EmailSender.java
index 198a2eb..f877937 100644
--- a/api/src/main/java/com/ning/billing/util/email/EmailSender.java
+++ b/api/src/main/java/com/ning/billing/util/email/EmailSender.java
@@ -20,5 +20,8 @@ import java.io.IOException;
 import java.util.List;
 
 public interface EmailSender {
-    public void sendSecureEmail(List<String> to, List<String> cc, String subject, String htmlBody) throws IOException, EmailApiException;
+
+    public void sendHTMLEmail(List<String> to, List<String> cc, String subject, String htmlBody) throws IOException, EmailApiException;
+
+    public void sendPlainTextEmail(List<String> to, List<String> cc, String subject, String body) throws IOException, EmailApiException;
 }
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 3bb88f0..760b8df 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
@@ -328,6 +328,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
         invoiceSqlDao.inTransaction(new Transaction<Void, InvoiceSqlDao>() {
             @Override
             public Void inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+                // Note: the tagUserApi is audited
                 tagUserApi.addTag(invoiceId, ObjectType.INVOICE, ControlTagType.WRITTEN_OFF.getId(), context);
 
                 final Invoice invoice = transactional.getById(invoiceId.toString());
@@ -343,6 +344,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
         invoiceSqlDao.inTransaction(new Transaction<Void, InvoiceSqlDao>() {
             @Override
             public Void inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+                // Note: the tagUserApi is audited
                 tagUserApi.removeTag(invoiceId, ObjectType.INVOICE, ControlTagType.WRITTEN_OFF.getId(), context);
 
                 final Invoice invoice = transactional.getById(invoiceId.toString());
@@ -364,6 +366,7 @@ public class AuditedInvoiceDao implements InvoiceDao {
         return invoicePaymentSqlDao.inTransaction(new Transaction<InvoicePayment, InvoicePaymentSqlDao>() {
             @Override
             public InvoicePayment inTransaction(final InvoicePaymentSqlDao transactional, final TransactionStatus status) throws Exception {
+                final List<EntityAudit> audits = new ArrayList<EntityAudit>();
 
                 final InvoiceSqlDao transInvoiceDao = transactional.become(InvoiceSqlDao.class);
 
@@ -391,6 +394,8 @@ public class AuditedInvoiceDao implements InvoiceDao {
                         payment.getInvoiceId(), context.getCreatedDate(), requestedPositiveAmount.negate(),
                         payment.getCurrency(), paymentCookieId, payment.getId());
                 transactional.create(refund, context);
+                final Long refundRecordId = transactional.getRecordId(refund.getId().toString());
+                audits.add(new EntityAudit(TableName.REFUNDS, refundRecordId, ChangeType.INSERT));
 
                 // Retrieve invoice after the Refund
                 final Invoice invoice = transInvoiceDao.getById(payment.getInvoiceId().toString());
@@ -403,7 +408,6 @@ public class AuditedInvoiceDao implements InvoiceDao {
                 final BigDecimal invoiceBalanceAfterRefund = invoice.getBalance();
                 final InvoiceItemSqlDao transInvoiceItemDao = transInvoiceDao.become(InvoiceItemSqlDao.class);
 
-
                 // At this point, we created the refund which made the invoice balance positive and applied any existing
                 // available CBA to that invoice.
                 // We now need to adjust the invoice and/or invoice items if needed and specified.
@@ -414,6 +418,8 @@ public class AuditedInvoiceDao implements InvoiceDao {
                     if (requestedPositiveAmountToAdjust.compareTo(BigDecimal.ZERO) > 0) {
                         final InvoiceItem adjItem = new RefundAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(), requestedPositiveAmountToAdjust.negate(), invoice.getCurrency());
                         transInvoiceItemDao.create(adjItem, context);
+                        final Long adjItemRecordId = transInvoiceItemDao.getRecordId(adjItem.getId().toString());
+                        audits.add(new EntityAudit(TableName.INVOICE_ITEMS, adjItemRecordId, ChangeType.INSERT));
                     }
                 } else if (isInvoiceAdjusted) {
                     // Invoice item adjustment
@@ -422,12 +428,17 @@ public class AuditedInvoiceDao implements InvoiceDao {
                         final InvoiceItem item = createAdjustmentItem(transInvoiceDao, invoice.getId(), invoiceItemId, adjAmount,
                                 invoice.getCurrency(), context.getCreatedDate().toLocalDate());
                         transInvoiceItemDao.create(item, context);
+                        final Long itemRecordId = transInvoiceItemDao.getRecordId(item.getId().toString());
+                        audits.add(new EntityAudit(TableName.INVOICE_ITEMS, itemRecordId, ChangeType.INSERT));
                     }
                 }
 
                 // Notify the bus since the balance of the invoice changed
                 notifyBusOfInvoiceAdjustment(transactional, invoice.getId(), invoice.getAccountId(), context.getUserToken());
 
+                // Save audit logs
+                transactional.insertAuditFromTransaction(audits, context);
+
                 return refund;
             }
         });
@@ -616,11 +627,16 @@ public class AuditedInvoiceDao implements InvoiceDao {
         return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
             @Override
             public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+                final List<EntityAudit> audits = new ArrayList<EntityAudit>();
+
                 UUID invoiceIdForExternalCharge = invoiceId;
                 // Create an invoice for that external charge if it doesn't exist
                 if (invoiceIdForExternalCharge == null) {
                     final Invoice invoiceForExternalCharge = new DefaultInvoice(accountId, effectiveDate, effectiveDate, currency);
                     transactional.create(invoiceForExternalCharge, context);
+                    final Long invoiceRecordId = transactional.getRecordId(invoiceForExternalCharge.getId().toString());
+                    audits.add(new EntityAudit(TableName.INVOICES, invoiceRecordId, ChangeType.INSERT));
+
                     invoiceIdForExternalCharge = invoiceForExternalCharge.getId();
                 }
 
@@ -630,6 +646,8 @@ public class AuditedInvoiceDao implements InvoiceDao {
 
                 final InvoiceItemSqlDao transInvoiceItemDao = transactional.become(InvoiceItemSqlDao.class);
                 transInvoiceItemDao.create(externalCharge, context);
+                final Long invoiceItemRecordId = transInvoiceItemDao.getRecordId(externalCharge.getId().toString());
+                audits.add(new EntityAudit(TableName.INVOICE_ITEMS, invoiceItemRecordId, ChangeType.INSERT));
 
                 // At this point, reread the invoice and figure out if we need to consume some of the CBA
                 final Invoice invoice = transactional.getById(invoiceIdForExternalCharge.toString());
@@ -643,10 +661,16 @@ public class AuditedInvoiceDao implements InvoiceDao {
                     final BigDecimal cbaAmountToConsume = accountCbaAvailable.compareTo(invoice.getBalance()) > 0 ? invoice.getBalance().negate() : accountCbaAvailable.negate();
                     final InvoiceItem cbaAdjItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(), cbaAmountToConsume, invoice.getCurrency());
                     transInvoiceItemDao.create(cbaAdjItem, context);
+                    final Long cbaAdjItemRecordId = transInvoiceItemDao.getRecordId(cbaAdjItem.getId().toString());
+                    audits.add(new EntityAudit(TableName.INVOICE_ITEMS, cbaAdjItemRecordId, ChangeType.INSERT));
                 }
 
                 // Notify the bus since the balance of the invoice changed
                 notifyBusOfInvoiceAdjustment(transactional, invoiceId, accountId, context.getUserToken());
+
+                // Save audit logs
+                transactional.insertAuditFromTransaction(audits, context);
+
                 return externalCharge;
             }
         });
@@ -663,25 +687,29 @@ public class AuditedInvoiceDao implements InvoiceDao {
         return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
             @Override
             public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+                final List<EntityAudit> audits = new ArrayList<EntityAudit>();
+
                 UUID invoiceIdForCredit = invoiceId;
                 // Create an invoice for that credit if it doesn't exist
                 if (invoiceIdForCredit == null) {
                     final Invoice invoiceForCredit = new DefaultInvoice(accountId, effectiveDate, effectiveDate, currency);
                     transactional.create(invoiceForCredit, context);
+                    final Long invoiceForCreditRecordId = transactional.getRecordId(invoiceForCredit.getId().toString());
+                    audits.add(new EntityAudit(TableName.INVOICES, invoiceForCreditRecordId, ChangeType.INSERT));
+
                     invoiceIdForCredit = invoiceForCredit.getId();
                 }
 
                 // Note! The amount is negated here!
                 final InvoiceItem credit = new CreditAdjInvoiceItem(invoiceIdForCredit, accountId, effectiveDate, positiveCreditAmount.negate(), currency);
-                final Long recordId = insertItemAndAddCBAIfNeeded(transactional, credit, context);
-
-                // Add audit
-                final EntityAudit audit = new EntityAudit(TableName.INVOICE_ITEMS, recordId, ChangeType.INSERT);
-                transactional.insertAuditFromTransaction(audit, context);
+                insertItemAndAddCBAIfNeeded(transactional, credit, audits, context);
 
                 // Notify the bus since the balance of the invoice changed
                 notifyBusOfInvoiceAdjustment(transactional, invoiceId, accountId, context.getUserToken());
 
+                // Save audit logs
+                transactional.insertAuditFromTransaction(audits, context);
+
                 return credit;
             }
         });
@@ -694,12 +722,15 @@ public class AuditedInvoiceDao implements InvoiceDao {
         return invoiceSqlDao.inTransaction(new Transaction<InvoiceItem, InvoiceSqlDao>() {
             @Override
             public InvoiceItem inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+                final List<EntityAudit> audits = new ArrayList<EntityAudit>();
                 final InvoiceItem invoiceItemAdjustment = createAdjustmentItem(transactional, invoiceId, invoiceItemId, positiveAdjAmount,
-                        currency, effectiveDate);
-                insertItemAndAddCBAIfNeeded(transactional, invoiceItemAdjustment, context);
-
+                                                                               currency, effectiveDate);
+                insertItemAndAddCBAIfNeeded(transactional, invoiceItemAdjustment, audits, context);
                 notifyBusOfInvoiceAdjustment(transactional, invoiceId, accountId, context.getUserToken());
 
+                // Save audit logs
+                transactional.insertAuditFromTransaction(audits, context);
+
                 return invoiceItemAdjustment;
             }
         });
@@ -710,6 +741,8 @@ public class AuditedInvoiceDao implements InvoiceDao {
         invoiceSqlDao.inTransaction(new Transaction<Void, InvoiceSqlDao>() {
             @Override
             public Void inTransaction(final InvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+                final List<EntityAudit> audits = new ArrayList<EntityAudit>();
+
                 // 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)) {
@@ -727,6 +760,14 @@ public class AuditedInvoiceDao implements InvoiceDao {
                 final InvoiceItem cbaAdjItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
                         cbaItem.getId(), cbaItem.getAmount().negate(), cbaItem.getCurrency());
                 invoiceItemSqlDao.create(cbaAdjItem, context);
+                final Long cbaAdjItemRecordId = invoiceItemSqlDao.getRecordId(cbaAdjItem.getId().toString());
+                audits.add(new EntityAudit(TableName.INVOICE_ITEMS, cbaAdjItemRecordId, ChangeType.INSERT));
+
+                // 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
@@ -778,12 +819,18 @@ public class AuditedInvoiceDao implements InvoiceDao {
                         final InvoiceItem nextCBAAdjItem = new CreditBalanceAdjInvoiceItem(invoiceFollowing.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
                                 cbaItem.getId(), positiveCBAAdjItemAmount, cbaItem.getCurrency());
                         invoiceItemSqlDao.create(nextCBAAdjItem, context);
+                        final Long nextCBAAdjItemRecordId = invoiceItemSqlDao.getRecordId(nextCBAAdjItem.getId().toString());
+                        audits.add(new EntityAudit(TableName.INVOICE_ITEMS, nextCBAAdjItemRecordId, ChangeType.INSERT));
+
                         if (positiveRemainderToAdjust.compareTo(BigDecimal.ZERO) == 0) {
                             break;
                         }
                     }
                 }
 
+                // Save audit logs
+                transactional.insertAuditFromTransaction(audits, context);
+
                 return null;
             }
         });
@@ -833,15 +880,18 @@ public class AuditedInvoiceDao implements InvoiceDao {
      *
      * @param transactional the InvoiceSqlDao
      * @param item          the invoice item to create
+     * @param audits        the audits to populate
      * @param context       the call context
      */
-    private Long insertItemAndAddCBAIfNeeded(final InvoiceSqlDao transactional, final InvoiceItem item, final CallContext context) {
+    private void insertItemAndAddCBAIfNeeded(final InvoiceSqlDao transactional, final InvoiceItem item,
+                                             final List<EntityAudit> audits, final CallContext context) {
         final InvoiceItemSqlDao transInvoiceItemDao = transactional.become(InvoiceItemSqlDao.class);
         transInvoiceItemDao.create(item, context);
 
-        addCBAIfNeeded(transactional, item.getInvoiceId(), context);
+        final Long invoiceItemRecordId = transInvoiceItemDao.getRecordId(item.getId().toString());
+        audits.add(new EntityAudit(TableName.INVOICE_ITEMS, invoiceItemRecordId, ChangeType.INSERT));
 
-        return transInvoiceItemDao.getRecordId(item.getId().toString());
+        addCBAIfNeeded(transactional, item.getInvoiceId(), audits, context);
     }
 
     /**
@@ -849,9 +899,11 @@ public class AuditedInvoiceDao implements InvoiceDao {
      *
      * @param transactional the InvoiceSqlDao
      * @param invoiceId     the invoice id to adjust
+     * @param audits        the audits to populate
      * @param context       the call context
      */
-    private void addCBAIfNeeded(final InvoiceSqlDao transactional, final UUID invoiceId, final CallContext context) {
+    private void addCBAIfNeeded(final InvoiceSqlDao transactional, final UUID invoiceId,
+                                final List<EntityAudit> audits, final CallContext context) {
         final Invoice invoice = transactional.getById(invoiceId.toString());
         if (invoice != null) {
             populateChildren(invoice, transactional);
@@ -865,7 +917,8 @@ public class AuditedInvoiceDao implements InvoiceDao {
             final InvoiceItem cbaAdjItem = new CreditBalanceAdjInvoiceItem(invoice.getId(), invoice.getAccountId(), context.getCreatedDate().toLocalDate(),
                     invoice.getBalance().negate(), invoice.getCurrency());
             transInvoiceItemDao.create(cbaAdjItem, context);
-
+            final Long cbaAdjItemRecordId = transInvoiceItemDao.getRecordId(cbaAdjItem.getId().toString());
+            audits.add(new EntityAudit(TableName.INVOICE_ITEMS, cbaAdjItemRecordId, ChangeType.INSERT));
         }
     }
 
diff --git a/invoice/src/main/java/com/ning/billing/invoice/notification/EmailInvoiceNotifier.java b/invoice/src/main/java/com/ning/billing/invoice/notification/EmailInvoiceNotifier.java
index 10d7202..aa63fe3 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/notification/EmailInvoiceNotifier.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/notification/EmailInvoiceNotifier.java
@@ -86,7 +86,7 @@ public class EmailInvoiceNotifier implements InvoiceNotifier {
 
         final EmailSender sender = new DefaultEmailSender(config);
         try {
-            sender.sendSecureEmail(to, cc, subject, htmlBody);
+            sender.sendHTMLEmail(to, cc, subject, htmlBody);
         } catch (EmailApiException e) {
             throw new InvoiceApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
         } catch (IOException e) {
diff --git a/invoice/src/main/java/com/ning/billing/invoice/template/formatters/DefaultInvoiceFormatter.java b/invoice/src/main/java/com/ning/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
index b6edb8d..34ceac6 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
@@ -101,10 +101,11 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
                 invoiceItems.add(item);
             }
         }
-        if (mergedCBAItem != null) {
+        // Don't display adjustments of zero
+        if (mergedCBAItem != null && mergedCBAItem.getAmount().compareTo(BigDecimal.ZERO) != 0) {
             invoiceItems.add(mergedCBAItem);
         }
-        if (mergedInvoiceAdjustment != null) {
+        if (mergedInvoiceAdjustment != null && mergedInvoiceAdjustment.getAmount().compareTo(BigDecimal.ZERO) != 0) {
             invoiceItems.add(mergedInvoiceAdjustment);
         }
 
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 2d3b95b..1374148 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;
 
@@ -1550,6 +1551,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);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java b/invoice/src/test/java/com/ning/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
index 0f12045..b1a6db5 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
@@ -56,6 +56,39 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuite {
     }
 
     @Test(groups = "fast")
+    public void testIgnoreZeroAdjustments() throws Exception {
+        // Scenario: single item with payment
+        // * $10 item
+        // * $-10 CBA
+        // * $10 CBA
+        final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+                                                                          UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+                                                                          new LocalDate(), BigDecimal.TEN, Currency.USD);
+        final CreditBalanceAdjInvoiceItem creditBalanceAdjInvoiceItem = new CreditBalanceAdjInvoiceItem(fixedItem.getInvoiceId(), fixedItem.getAccountId(),
+                                                                                                        fixedItem.getStartDate(), fixedItem.getAmount(),
+                                                                                                        fixedItem.getCurrency());
+        final CreditBalanceAdjInvoiceItem creditBalanceAdjInvoiceItem2 = new CreditBalanceAdjInvoiceItem(fixedItem.getInvoiceId(), fixedItem.getAccountId(),
+                                                                                                         fixedItem.getStartDate(), fixedItem.getAmount().negate(),
+                                                                                                         fixedItem.getCurrency());
+        final Invoice invoice = new DefaultInvoice(fixedItem.getInvoiceId(), fixedItem.getAccountId(), null,
+                                                   new LocalDate(), new LocalDate(), Currency.USD, false);
+        invoice.addInvoiceItem(fixedItem);
+        invoice.addInvoiceItem(creditBalanceAdjInvoiceItem);
+        invoice.addInvoiceItem(creditBalanceAdjInvoiceItem2);
+
+        // Check the scenario
+        Assert.assertEquals(invoice.getBalance().doubleValue(), 10.00);
+        Assert.assertEquals(invoice.getCBAAmount().doubleValue(), 0.00);
+
+        // Verify the merge
+        final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US);
+        final List<InvoiceItem> invoiceItems = formatter.getInvoiceItems();
+        Assert.assertEquals(invoiceItems.size(), 1);
+        Assert.assertEquals(invoiceItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+        Assert.assertEquals(invoiceItems.get(0).getAmount().doubleValue(), 10.00);
+    }
+
+    @Test(groups = "fast")
     public void testMergeItems() throws Exception {
         // Scenario: single item with payment
         // * $10 item
diff --git a/overdue/src/main/java/com/ning/billing/overdue/applicator/OverdueEmailGenerator.java b/overdue/src/main/java/com/ning/billing/overdue/applicator/OverdueEmailGenerator.java
new file mode 100644
index 0000000..ec2f971
--- /dev/null
+++ b/overdue/src/main/java/com/ning/billing/overdue/applicator/OverdueEmailGenerator.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning 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:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.overdue.applicator;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.junction.api.Blockable;
+import com.ning.billing.overdue.OverdueState;
+import com.ning.billing.overdue.config.api.BillingState;
+import com.ning.billing.util.email.templates.TemplateEngine;
+
+import com.google.inject.Inject;
+
+public class OverdueEmailGenerator {
+
+    private final TemplateEngine templateEngine;
+
+    @Inject
+    public OverdueEmailGenerator(final TemplateEngine templateEngine) {
+        this.templateEngine = templateEngine;
+    }
+
+    public <T extends Blockable> String generateEmail(final Account account, final BillingState<T> billingState,
+                                                      final T overdueable, final OverdueState<T> nextOverdueState) throws IOException {
+        final Map<String, Object> data = new HashMap<String, Object>();
+
+        // TODO raw objects for now. We eventually should respect the account locale and support translations
+        data.put("account", account);
+        data.put("billingState", billingState);
+        data.put("overdueable", overdueable);
+        data.put("nextOverdueState", nextOverdueState);
+
+        // TODO single template for all languages for now
+        return templateEngine.executeTemplate(nextOverdueState.getEnterStateEmailNotification().getTemplateName(), data);
+    }
+}
diff --git a/overdue/src/main/java/com/ning/billing/overdue/applicator/OverdueStateApplicator.java b/overdue/src/main/java/com/ning/billing/overdue/applicator/OverdueStateApplicator.java
index 40a93bd..7492ae9 100644
--- a/overdue/src/main/java/com/ning/billing/overdue/applicator/OverdueStateApplicator.java
+++ b/overdue/src/main/java/com/ning/billing/overdue/applicator/OverdueStateApplicator.java
@@ -16,8 +16,10 @@
 
 package com.ning.billing.overdue.applicator;
 
+import java.io.IOException;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
@@ -25,15 +27,17 @@ import org.joda.time.Period;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.inject.Inject;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.account.api.Account;
+import com.ning.billing.account.api.AccountApiException;
+import com.ning.billing.account.api.AccountUserApi;
 import com.ning.billing.catalog.api.ActionPolicy;
 import com.ning.billing.entitlement.api.user.EntitlementUserApi;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.junction.api.Blockable;
+import com.ning.billing.junction.api.Blockable.Type;
 import com.ning.billing.junction.api.BlockingApi;
 import com.ning.billing.junction.api.BlockingApiException;
 import com.ning.billing.junction.api.DefaultBlockingState;
@@ -51,6 +55,14 @@ import com.ning.billing.util.callcontext.CallContextFactory;
 import com.ning.billing.util.callcontext.CallOrigin;
 import com.ning.billing.util.callcontext.UserType;
 import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.email.DefaultEmailSender;
+import com.ning.billing.util.email.EmailApiException;
+import com.ning.billing.util.email.EmailConfig;
+import com.ning.billing.util.email.EmailSender;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+import com.samskivert.mustache.MustacheException;
 
 public class OverdueStateApplicator<T extends Blockable> {
 
@@ -62,45 +74,51 @@ public class OverdueStateApplicator<T extends Blockable> {
     private final Clock clock;
     private final OverdueCheckPoster poster;
     private final Bus bus;
+    private final AccountUserApi accountUserApi;
     private final EntitlementUserApi entitlementUserApi;
     private final CallContextFactory factory;
-
+    private final OverdueEmailGenerator overdueEmailGenerator;
+    private final EmailSender emailSender;
 
     @Inject
-    public OverdueStateApplicator(final BlockingApi accessApi, final EntitlementUserApi entitlementUserApi, final Clock clock,
-            final OverdueCheckPoster poster, final Bus bus, final CallContextFactory factory) {
+    public OverdueStateApplicator(final BlockingApi accessApi, final AccountUserApi accountUserApi, final EntitlementUserApi entitlementUserApi,
+                                  final Clock clock, final OverdueCheckPoster poster, final OverdueEmailGenerator overdueEmailGenerator,
+                                  final EmailConfig config, final Bus bus, final CallContextFactory factory) {
         this.blockingApi = accessApi;
+        this.accountUserApi = accountUserApi;
         this.entitlementUserApi = entitlementUserApi;
         this.clock = clock;
         this.poster = poster;
+        this.overdueEmailGenerator = overdueEmailGenerator;
+        this.emailSender = new DefaultEmailSender(config);
         this.bus = bus;
         this.factory = factory;
     }
 
-
     public void apply(final OverdueState<T> firstOverdueState, final BillingState<T> billingState,
-            final T overdueable, final String previousOverdueStateName, final OverdueState<T> nextOverdueState) throws OverdueException {
-
+                      final T overdueable, final String previousOverdueStateName, final OverdueState<T> nextOverdueState) throws OverdueException {
         try {
-
             // We did not reach first state, we we need to check if there is any pending condition for which we will not receive
             // any notifications.. (last two conditions are there for test purpose)
-            if (nextOverdueState.isClearState() && firstOverdueState != null && billingState !=  null) {
-                 final LocalDate firstUnpaidInvoice = billingState.getDateOfEarliestUnpaidInvoice();
-                 if (firstUnpaidInvoice != null) {
-                     final Period reevaluationInterval = firstOverdueState.getReevaluationInterval();
-                     createFutureNotification(overdueable, firstUnpaidInvoice.toDateTimeAtCurrentTime().plus(reevaluationInterval));
-                 }
+            if (nextOverdueState.isClearState() && firstOverdueState != null && billingState != null) {
+                final LocalDate firstUnpaidInvoice = billingState.getDateOfEarliestUnpaidInvoice();
+                if (firstUnpaidInvoice != null) {
+                    final Period reevaluationInterval = firstOverdueState.getReevaluationInterval();
+                    createFutureNotification(overdueable, firstUnpaidInvoice.toDateTimeAtCurrentTime().plus(reevaluationInterval));
+                }
             }
 
-            if (nextOverdueState == null || previousOverdueStateName.equals(nextOverdueState.getName())) {
-                return; //That's it we are done...
+            if (previousOverdueStateName.equals(nextOverdueState.getName())) {
+                return; // That's it, we are done...
             }
 
             storeNewState(overdueable, nextOverdueState);
 
             cancelSubscriptionsIfRequired(overdueable, nextOverdueState);
 
+            sendEmailIfRequired(billingState, overdueable, nextOverdueState);
+
+            // Add entry in notification queue
             final Period reevaluationInterval = nextOverdueState.getReevaluationInterval();
             if (!nextOverdueState.isClearState()) {
                 createFutureNotification(overdueable, clock.getUTCNow().plus(reevaluationInterval));
@@ -118,12 +136,11 @@ public class OverdueStateApplicator<T extends Blockable> {
         try {
             bus.post(createOverdueEvent(overdueable, previousOverdueStateName, nextOverdueState.getName()));
         } catch (Exception e) {
-            log.error("Error posting overdue change event to bus",e);
+            log.error("Error posting overdue change event to bus", e);
         }
     }
 
-
-    private OverdueChangeEvent createOverdueEvent(T overdueable, String previousOverdueStateName, String nextOverdueStateName) throws BlockingApiException {
+    private OverdueChangeEvent createOverdueEvent(final T overdueable, final String previousOverdueStateName, final String nextOverdueStateName) throws BlockingApiException {
         return new DefaultOverdueChangeEvent(overdueable.getId(), Blockable.Type.get(overdueable), previousOverdueStateName, nextOverdueStateName, null);
     }
 
@@ -148,14 +165,12 @@ public class OverdueStateApplicator<T extends Blockable> {
         return nextOverdueState.disableEntitlementAndChangesBlocked();
     }
 
-    protected void createFutureNotification(final T overdueable,
-                                            final DateTime timeOfNextCheck) {
+    protected void createFutureNotification(final T overdueable, final DateTime timeOfNextCheck) {
         poster.insertOverdueCheckNotification(overdueable, timeOfNextCheck);
-
     }
 
     protected void clear(final T blockable) {
-        //Need to clear the overrride table here too (when we add it)
+        // Need to clear the override table here too (when we add it)
         poster.clearNotificationsFor(blockable);
     }
 
@@ -164,21 +179,21 @@ public class OverdueStateApplicator<T extends Blockable> {
             return;
         }
         try {
-            ActionPolicy actionPolicy = null;
-            switch(nextOverdueState.getSubscriptionCancellationPolicy()) {
-            case END_OF_TERM:
-                actionPolicy = ActionPolicy.END_OF_TERM;
-                break;
-            case IMMEDIATE:
-                actionPolicy = ActionPolicy.IMMEDIATE;
-                break;
-            default :
-                throw new IllegalStateException("Unexpected OverdueCancellationPolicy " + nextOverdueState.getSubscriptionCancellationPolicy());
+            final ActionPolicy actionPolicy;
+            switch (nextOverdueState.getSubscriptionCancellationPolicy()) {
+                case END_OF_TERM:
+                    actionPolicy = ActionPolicy.END_OF_TERM;
+                    break;
+                case IMMEDIATE:
+                    actionPolicy = ActionPolicy.IMMEDIATE;
+                    break;
+                default:
+                    throw new IllegalStateException("Unexpected OverdueCancellationPolicy " + nextOverdueState.getSubscriptionCancellationPolicy());
             }
             final List<Subscription> toBeCancelled = new LinkedList<Subscription>();
             computeSubscriptionsToCancel(blockable, toBeCancelled);
             final CallContext context = factory.createCallContext(API_USER_NAME, CallOrigin.INTERNAL, UserType.SYSTEM);
-            for (Subscription cur : toBeCancelled) {
+            for (final Subscription cur : toBeCancelled) {
                 cur.cancelWithPolicy(clock.getUTCNow(), actionPolicy, context);
             }
         } catch (EntitlementUserApiException e) {
@@ -187,18 +202,75 @@ public class OverdueStateApplicator<T extends Blockable> {
     }
 
     @SuppressWarnings("unchecked")
-    private void computeSubscriptionsToCancel(final T blockable, final List<Subscription> result) throws EntitlementUserApiException{
+    private void computeSubscriptionsToCancel(final T blockable, final List<Subscription> result) throws EntitlementUserApiException {
         if (blockable instanceof Subscription) {
             result.add((Subscription) blockable);
-            return;
         } else if (blockable instanceof SubscriptionBundle) {
-            for (Subscription cur : entitlementUserApi.getSubscriptionsForBundle(blockable.getId())) {
+            for (final Subscription cur : entitlementUserApi.getSubscriptionsForBundle(blockable.getId())) {
                 computeSubscriptionsToCancel((T) cur, result);
             }
         } else if (blockable instanceof Account) {
-            for (SubscriptionBundle cur : entitlementUserApi.getBundlesForAccount(blockable.getId())) {
+            for (final SubscriptionBundle cur : entitlementUserApi.getBundlesForAccount(blockable.getId())) {
                 computeSubscriptionsToCancel((T) cur, result);
             }
         }
     }
+
+    private void sendEmailIfRequired(final BillingState<T> billingState, final T overdueable, final OverdueState<T> nextOverdueState) {
+        // Note: we don't want to fail the full refresh call because sending the email failed.
+        // That's the reason why we catch all exceptions here.
+        // The alternative would be to: throw new OverdueApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
+
+        // If sending is not configured, skip
+        if (nextOverdueState.getEnterStateEmailNotification() == null) {
+            return;
+        }
+
+        // Retrieve the account
+        final Account account;
+        final Type overdueableType = Blockable.Type.get(overdueable);
+        try {
+            if (Type.SUBSCRIPTION.equals(overdueableType)) {
+                final UUID bundleId = ((Subscription) overdueable).getBundleId();
+                final SubscriptionBundle bundle = entitlementUserApi.getBundleFromId(bundleId);
+                account = accountUserApi.getAccountById(bundle.getAccountId());
+            } else if (Type.SUBSCRIPTION_BUNDLE.equals(overdueableType)) {
+                final UUID bundleId = ((SubscriptionBundle) overdueable).getId();
+                final SubscriptionBundle bundle = entitlementUserApi.getBundleFromId(bundleId);
+                account = accountUserApi.getAccountById(bundle.getAccountId());
+            } else if (Type.ACCOUNT.equals(overdueableType)) {
+                account = (Account) overdueable;
+            } else {
+                log.warn("Unable to retrieve account for overdueable {} (type {})", overdueable.getId(), overdueableType);
+                return;
+            }
+        } catch (EntitlementUserApiException e) {
+            log.warn(String.format("Unable to retrieve account for overdueable %s (type %s)", overdueable.getId(), overdueableType), e);
+            return;
+        } catch (AccountApiException e) {
+            log.warn(String.format("Unable to retrieve account for overdueable %s (type %s)", overdueable.getId(), overdueableType), e);
+            return;
+        }
+
+        final List<String> to = ImmutableList.<String>of(account.getEmail());
+        // TODO - should we look at the account CC: list?
+        final List<String> cc = ImmutableList.<String>of();
+        final String subject = nextOverdueState.getEnterStateEmailNotification().getSubject();
+
+        try {
+            // Generate and send the email
+            final String emailBody = overdueEmailGenerator.generateEmail(account, billingState, overdueable, nextOverdueState);
+            if (nextOverdueState.getEnterStateEmailNotification().isHTML()) {
+                emailSender.sendHTMLEmail(to, cc, subject, emailBody);
+            } else {
+                emailSender.sendPlainTextEmail(to, cc, subject, emailBody);
+            }
+        } catch (IOException e) {
+            log.warn(String.format("Unable to generate or send overdue notification email for account %s and overdueable %s", account.getId(), overdueable.getId()), e);
+        } catch (EmailApiException e) {
+            log.warn(String.format("Unable to send overdue notification email for account %s and overdueable %s", account.getId(), overdueable.getId()), e);
+        } catch (MustacheException e) {
+            log.warn(String.format("Unable to generate overdue notification email for account %s and overdueable %s", account.getId(), overdueable.getId()), e);
+        }
+    }
 }
diff --git a/overdue/src/main/java/com/ning/billing/overdue/config/DefaultEmailNotification.java b/overdue/src/main/java/com/ning/billing/overdue/config/DefaultEmailNotification.java
new file mode 100644
index 0000000..a5491d3
--- /dev/null
+++ b/overdue/src/main/java/com/ning/billing/overdue/config/DefaultEmailNotification.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning 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:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.overdue.config;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+
+import com.ning.billing.overdue.EmailNotification;
+
+@XmlAccessorType(XmlAccessType.NONE)
+public class DefaultEmailNotification implements EmailNotification {
+
+    @XmlElement(required = true, name = "subject")
+    private String subject;
+
+    @XmlElement(required = true, name = "templateName")
+    private String templateName;
+
+    @XmlElement(required = false, name = "isHTML")
+    private Boolean isHTML = false;
+
+    @Override
+    public String getSubject() {
+        return subject;
+    }
+
+    @Override
+    public String getTemplateName() {
+        return templateName;
+    }
+
+    @Override
+    public Boolean isHTML() {
+        return isHTML;
+    }
+}
diff --git a/overdue/src/main/java/com/ning/billing/overdue/config/DefaultOverdueState.java b/overdue/src/main/java/com/ning/billing/overdue/config/DefaultOverdueState.java
index 383b3fd..d95b487 100644
--- a/overdue/src/main/java/com/ning/billing/overdue/config/DefaultOverdueState.java
+++ b/overdue/src/main/java/com/ning/billing/overdue/config/DefaultOverdueState.java
@@ -27,6 +27,7 @@ import org.joda.time.Period;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.catalog.api.TimeUnit;
 import com.ning.billing.junction.api.Blockable;
+import com.ning.billing.overdue.EmailNotification;
 import com.ning.billing.overdue.OverdueApiException;
 import com.ning.billing.overdue.OverdueCancellationPolicicy;
 import com.ning.billing.overdue.OverdueState;
@@ -39,7 +40,6 @@ public class DefaultOverdueState<T extends Blockable> extends ValidatingConfig<O
 
     private static final int MAX_NAME_LENGTH = 50;
 
-
     @XmlElement(required = false, name = "condition")
     private DefaultCondition<T> condition;
 
@@ -65,26 +65,21 @@ public class DefaultOverdueState<T extends Blockable> extends ValidatingConfig<O
     @XmlElement(required = false, name = "autoReevaluationInterval")
     private DefaultDuration autoReevaluationInterval;
 
+    @XmlElement(required = false, name = "enterStateEmailNotification")
+    private DefaultEmailNotification enterStateEmailNotification;
 
     //Other actions could include
-    // - send email
     // - trigger payment retry?
     // - add tagStore to bundle/account
     // - set payment failure email template
     // - set payment retry interval
     // - backup payment mechanism?
 
-    /* (non-Javadoc)
-    * @see com.ning.billing.catalog.overdue.OverdueState#getStageName()
-    */
     @Override
     public String getName() {
         return name;
     }
 
-    /* (non-Javadoc)
-    * @see com.ning.billing.catalog.overdue.OverdueState#getExternalMessage()
-    */
     @Override
     public String getExternalMessage() {
         return externalMessage;
@@ -95,9 +90,6 @@ public class DefaultOverdueState<T extends Blockable> extends ValidatingConfig<O
         return blockChanges || disableEntitlement;
     }
 
-    /* (non-Javadoc)
-    * @see com.ning.billing.catalog.overdue.OverdueState#applyCancel()
-    */
     @Override
     public boolean disableEntitlementAndChangesBlocked() {
         return disableEntitlement;
@@ -146,7 +138,6 @@ public class DefaultOverdueState<T extends Blockable> extends ValidatingConfig<O
         return this;
     }
 
-
     protected DefaultOverdueState<T> setBlockChanges(final boolean cancel) {
         this.blockChanges = cancel;
         return this;
@@ -173,7 +164,11 @@ public class DefaultOverdueState<T extends Blockable> extends ValidatingConfig<O
 
     @Override
     public int getDaysBetweenPaymentRetries() {
-        final Integer daysBetweenPaymentRetries = 8;
-        return daysBetweenPaymentRetries;
+        return 8;
+    }
+
+    @Override
+    public EmailNotification getEnterStateEmailNotification() {
+        return enterStateEmailNotification;
     }
 }
diff --git a/overdue/src/main/java/com/ning/billing/overdue/glue/DefaultOverdueModule.java b/overdue/src/main/java/com/ning/billing/overdue/glue/DefaultOverdueModule.java
index 8536748..baf8700 100644
--- a/overdue/src/main/java/com/ning/billing/overdue/glue/DefaultOverdueModule.java
+++ b/overdue/src/main/java/com/ning/billing/overdue/glue/DefaultOverdueModule.java
@@ -18,7 +18,6 @@ package com.ning.billing.overdue.glue;
 
 import org.skife.config.ConfigurationObjectFactory;
 
-import com.google.inject.AbstractModule;
 import com.ning.billing.glue.OverdueModule;
 import com.ning.billing.ovedue.notification.DefaultOverdueCheckNotifier;
 import com.ning.billing.ovedue.notification.DefaultOverdueCheckPoster;
@@ -28,10 +27,12 @@ import com.ning.billing.overdue.OverdueProperties;
 import com.ning.billing.overdue.OverdueService;
 import com.ning.billing.overdue.OverdueUserApi;
 import com.ning.billing.overdue.api.DefaultOverdueUserApi;
+import com.ning.billing.overdue.applicator.OverdueEmailGenerator;
 import com.ning.billing.overdue.service.DefaultOverdueService;
 import com.ning.billing.overdue.service.ExtendedOverdueService;
 import com.ning.billing.overdue.wrapper.OverdueWrapperFactory;
 
+import com.google.inject.AbstractModule;
 
 public class DefaultOverdueModule extends AbstractModule implements OverdueModule {
 
@@ -42,13 +43,13 @@ public class DefaultOverdueModule extends AbstractModule implements OverdueModul
         // internal bindings
         installOverdueService();
         installOverdueWrapperFactory();
+        installOverdueEmail();
 
         final OverdueProperties config = new ConfigurationObjectFactory(System.getProperties()).build(OverdueProperties.class);
         bind(OverdueProperties.class).toInstance(config);
         bind(ExtendedOverdueService.class).to(DefaultOverdueService.class).asEagerSingleton();
         bind(OverdueCheckNotifier.class).to(DefaultOverdueCheckNotifier.class).asEagerSingleton();
         bind(OverdueCheckPoster.class).to(DefaultOverdueCheckPoster.class).asEagerSingleton();
-        
     }
 
     protected void installOverdueService() {
@@ -59,13 +60,12 @@ public class DefaultOverdueModule extends AbstractModule implements OverdueModul
         bind(OverdueWrapperFactory.class).asEagerSingleton();
     }
 
-    /* (non-Javadoc)
-     * @see com.ning.billing.overdue.glue.OverdueModule#installOverdueUserApi()
-     */
+    protected void installOverdueEmail() {
+        bind(OverdueEmailGenerator.class).asEagerSingleton();
+    }
+
     @Override
     public void installOverdueUserApi() {
         bind(OverdueUserApi.class).to(DefaultOverdueUserApi.class).asEagerSingleton();
     }
-
-
 }
diff --git a/overdue/src/test/java/com/ning/billing/overdue/config/TestOverdueConfig.java b/overdue/src/test/java/com/ning/billing/overdue/config/TestOverdueConfig.java
index 2b2c264..534f12f 100644
--- a/overdue/src/test/java/com/ning/billing/overdue/config/TestOverdueConfig.java
+++ b/overdue/src/test/java/com/ning/billing/overdue/config/TestOverdueConfig.java
@@ -19,45 +19,59 @@ package com.ning.billing.overdue.config;
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.ning.billing.overdue.EmailNotification;
 import com.ning.billing.overdue.OverdueTestSuite;
 import com.ning.billing.util.config.XMLLoader;
 
 public class TestOverdueConfig extends OverdueTestSuite {
+
     @Test(groups = "fast")
     public void testParseConfig() throws Exception {
         final String xml = "<overdueConfig>" +
-                "   <bundleOverdueStates>" +
-                "       <state name=\"OD1\">" +
-                "           <condition>" +
-                "               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
-                "                   <unit>MONTHS</unit><number>1</number>" +
-                "               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
-                "           </condition>" +
-                "           <externalMessage>Reached OD1</externalMessage>" +
-                "           <blockChanges>true</blockChanges>" +
-                "           <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
-                "           <autoReevaluationInterval>" +
-                "               <unit>DAYS</unit><number>15</number>" +
-                "           </autoReevaluationInterval>" +
-                "       </state>" +
-                "       <state name=\"OD2\">" +
-                "           <condition>" +
-                "               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
-                "                   <unit>MONTHS</unit><number>2</number>" +
-                "               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
-                "           </condition>" +
-                "           <externalMessage>Reached OD1</externalMessage>" +
-                "           <blockChanges>true</blockChanges>" +
-                "           <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>" +
-                "           <autoReevaluationInterval>" +
-                "               <unit>DAYS</unit><number>15</number>" +
-                "           </autoReevaluationInterval>" +
-                "       </state>" +
-                "   </bundleOverdueStates>" +
-                "</overdueConfig>";
+                           "   <bundleOverdueStates>" +
+                           "       <state name=\"OD1\">" +
+                           "           <condition>" +
+                           "               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+                           "                   <unit>MONTHS</unit><number>1</number>" +
+                           "               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+                           "           </condition>" +
+                           "           <externalMessage>Reached OD1</externalMessage>" +
+                           "           <blockChanges>true</blockChanges>" +
+                           "           <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
+                           "           <autoReevaluationInterval>" +
+                           "               <unit>DAYS</unit><number>15</number>" +
+                           "           </autoReevaluationInterval>" +
+                           "       </state>" +
+                           "       <state name=\"OD2\">" +
+                           "           <condition>" +
+                           "               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+                           "                   <unit>MONTHS</unit><number>2</number>" +
+                           "               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+                           "           </condition>" +
+                           "           <externalMessage>Reached OD1</externalMessage>" +
+                           "           <blockChanges>true</blockChanges>" +
+                           "           <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>" +
+                           "           <autoReevaluationInterval>" +
+                           "               <unit>DAYS</unit><number>15</number>" +
+                           "           </autoReevaluationInterval>" +
+                           "           <enterStateEmailNotification>" +
+                           "               <subject>ToTo</subject><templateName>Titi</templateName>" +
+                           "           </enterStateEmailNotification>" +
+                           "       </state>" +
+                           "   </bundleOverdueStates>" +
+                           "</overdueConfig>";
         final InputStream is = new ByteArrayInputStream(xml.getBytes());
         final OverdueConfig c = XMLLoader.getObjectFromStreamNoValidation(is, OverdueConfig.class);
+        Assert.assertEquals(c.getBundleStateSet().size(), 2);
+
+        Assert.assertNull(c.getBundleStateSet().getStates()[0].getEnterStateEmailNotification());
+
+        final EmailNotification secondNotification = c.getBundleStateSet().getStates()[1].getEnterStateEmailNotification();
+        Assert.assertEquals(secondNotification.getSubject(), "ToTo");
+        Assert.assertEquals(secondNotification.getTemplateName(), "Titi");
+        Assert.assertFalse(secondNotification.isHTML());
     }
 }
diff --git a/overdue/src/test/java/com/ning/billing/overdue/notification/TestOverdueCheckNotifier.java b/overdue/src/test/java/com/ning/billing/overdue/notification/TestOverdueCheckNotifier.java
index d2cecf1..2cd0dc0 100644
--- a/overdue/src/test/java/com/ning/billing/overdue/notification/TestOverdueCheckNotifier.java
+++ b/overdue/src/test/java/com/ning/billing/overdue/notification/TestOverdueCheckNotifier.java
@@ -63,6 +63,8 @@ import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.clock.ClockMock;
 import com.ning.billing.util.customfield.dao.AuditedCustomFieldDao;
 import com.ning.billing.util.customfield.dao.CustomFieldDao;
+import com.ning.billing.util.email.EmailModule;
+import com.ning.billing.util.email.templates.TemplateModule;
 import com.ning.billing.util.globallocker.GlobalLocker;
 import com.ning.billing.util.globallocker.MySqlGlobalLocker;
 import com.ning.billing.util.glue.BusModule;
@@ -130,6 +132,8 @@ public class TestOverdueCheckNotifier extends OverdueTestSuiteWithEmbeddedDB {
                 bind(GlobalLocker.class).to(MySqlGlobalLocker.class).asEagerSingleton();
                 bind(ChargeThruApi.class).toInstance(Mockito.mock(ChargeThruApi.class));
                 install(new MockJunctionModule());
+                install(new EmailModule());
+                install(new TemplateModule());
             }
         });
 
diff --git a/overdue/src/test/java/com/ning/billing/overdue/OverdueTestBase.java b/overdue/src/test/java/com/ning/billing/overdue/OverdueTestBase.java
index 6b8d67e..477d1bb 100644
--- a/overdue/src/test/java/com/ning/billing/overdue/OverdueTestBase.java
+++ b/overdue/src/test/java/com/ning/billing/overdue/OverdueTestBase.java
@@ -60,11 +60,15 @@ import com.ning.billing.overdue.service.DefaultOverdueService;
 import com.ning.billing.overdue.wrapper.OverdueWrapperFactory;
 import com.ning.billing.util.bus.BusService;
 import com.ning.billing.util.clock.ClockMock;
+import com.ning.billing.util.email.EmailModule;
+import com.ning.billing.util.email.templates.TemplateModule;
 import com.ning.billing.util.glue.CallContextModule;
 import com.ning.billing.util.glue.NotificationQueueModule;
 import com.ning.billing.util.notificationq.NotificationQueueService.NotificationQueueAlreadyExists;
 
-@Guice(modules = {DefaultOverdueModule.class, OverdueListenerTesterModule.class, MockClockModule.class, ApplicatorMockJunctionModule.class, CallContextModule.class, CatalogModule.class, MockInvoiceModule.class, MockPaymentModule.class, NotificationQueueModule.class, TestDbiModule.class})
+@Guice(modules = {DefaultOverdueModule.class, OverdueListenerTesterModule.class, MockClockModule.class, ApplicatorMockJunctionModule.class,
+                  CallContextModule.class, CatalogModule.class, MockInvoiceModule.class, MockPaymentModule.class, NotificationQueueModule.class,
+                  EmailModule.class, TemplateModule.class, TestDbiModule.class})
 public abstract class OverdueTestBase extends OverdueTestSuiteWithEmbeddedDB {
     protected final String configXml =
             "<overdueConfig>" +
diff --git a/util/src/main/java/com/ning/billing/util/email/DefaultEmailSender.java b/util/src/main/java/com/ning/billing/util/email/DefaultEmailSender.java
index 48d6594..a238c8c 100644
--- a/util/src/main/java/com/ning/billing/util/email/DefaultEmailSender.java
+++ b/util/src/main/java/com/ning/billing/util/email/DefaultEmailSender.java
@@ -16,17 +16,22 @@
 
 package com.ning.billing.util.email;
 
+import java.io.IOException;
 import java.util.List;
 
+import org.apache.commons.mail.Email;
 import org.apache.commons.mail.EmailException;
 import org.apache.commons.mail.HtmlEmail;
+import org.apache.commons.mail.SimpleEmail;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.inject.Inject;
 import com.ning.billing.ErrorCode;
 
+import com.google.inject.Inject;
+
 public class DefaultEmailSender implements EmailSender {
+
     private final Logger log = LoggerFactory.getLogger(EmailSender.class);
     private final EmailConfig config;
 
@@ -36,11 +41,31 @@ public class DefaultEmailSender implements EmailSender {
     }
 
     @Override
-    public void sendSecureEmail(final List<String> to, final List<String> cc, final String subject, final String htmlBody) throws EmailApiException {
-        final HtmlEmail email;
+    public void sendHTMLEmail(final List<String> to, final List<String> cc, final String subject, final String htmlBody) throws EmailApiException {
+        final HtmlEmail email = new HtmlEmail();
+        try {
+            email.setHtmlMsg(htmlBody);
+        } catch (EmailException e) {
+            throw new EmailApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
+        }
+
+        sendEmail(to, cc, subject, email);
+    }
+
+    @Override
+    public void sendPlainTextEmail(final List<String> to, final List<String> cc, final String subject, final String body) throws IOException, EmailApiException {
+        final SimpleEmail email = new SimpleEmail();
         try {
-            email = new HtmlEmail();
+            email.setMsg(body);
+        } catch (EmailException e) {
+            throw new EmailApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
+        }
 
+        sendEmail(to, cc, subject, email);
+    }
+
+    private void sendEmail(final List<String> to, final List<String> cc, final String subject, final Email email) throws EmailApiException {
+        try {
             email.setSmtpPort(config.getSmtpPort());
             if (config.useSmtpAuth()) {
                 email.setAuthentication(config.getSmtpUserName(), config.getSmtpPassword());
@@ -49,7 +74,6 @@ public class DefaultEmailSender implements EmailSender {
             email.setFrom(config.getDefaultFrom());
 
             email.setSubject(subject);
-            email.setHtmlMsg(htmlBody);
 
             if (to != null) {
                 for (final String recipient : to) {
diff --git a/util/src/test/java/com/ning/billing/util/email/EmailSenderTest.java b/util/src/test/java/com/ning/billing/util/email/EmailSenderTest.java
index 81b40d9..0fb604f 100644
--- a/util/src/test/java/com/ning/billing/util/email/EmailSenderTest.java
+++ b/util/src/test/java/com/ning/billing/util/email/EmailSenderTest.java
@@ -39,6 +39,6 @@ public class EmailSenderTest extends UtilTestSuite {
         recipients.add("killbill.ning@gmail.com");
 
         final EmailSender sender = new DefaultEmailSender(config);
-        sender.sendSecureEmail(recipients, null, "Test message", html);
+        sender.sendHTMLEmail(recipients, null, "Test message", html);
     }
 }