killbill-memoizeit
Changes
invoice/src/main/java/com/ning/billing/invoice/template/formatters/DefaultInvoiceFormatter.java 5(+3 -2)
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);
}
}