killbill-aplcache

invoice: revisit notification queue integration Get notified

7/11/2012 11:17:09 PM

Details

diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
index fe88a63..3ed5c04 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
@@ -38,6 +38,7 @@ import com.ning.billing.invoice.api.InvoiceItem;
 import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.invoice.api.InvoicePayment;
 import com.ning.billing.invoice.api.InvoicePayment.InvoicePaymentType;
+import com.ning.billing.invoice.generator.InvoiceDateUtils;
 import com.ning.billing.invoice.model.CreditAdjInvoiceItem;
 import com.ning.billing.invoice.model.CreditBalanceAdjInvoiceItem;
 import com.ning.billing.invoice.model.DefaultInvoice;
@@ -49,6 +50,7 @@ import com.ning.billing.util.ChangeType;
 import com.ning.billing.util.api.TagApiException;
 import com.ning.billing.util.api.TagUserApi;
 import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.dao.EntityAudit;
 import com.ning.billing.util.dao.ObjectType;
 import com.ning.billing.util.dao.TableName;
@@ -60,16 +62,19 @@ public class DefaultInvoiceDao implements InvoiceDao {
     private final TagUserApi tagUserApi;
     private final NextBillingDatePoster nextBillingDatePoster;
     private final InvoiceItemSqlDao invoiceItemSqlDao;
+    private final Clock clock;
 
     @Inject
     public DefaultInvoiceDao(final IDBI dbi,
                              final NextBillingDatePoster nextBillingDatePoster,
-                             final TagUserApi tagUserApi) {
+                             final TagUserApi tagUserApi,
+                             final Clock clock) {
         this.invoiceSqlDao = dbi.onDemand(InvoiceSqlDao.class);
         this.invoicePaymentSqlDao = dbi.onDemand(InvoicePaymentSqlDao.class);
         this.invoiceItemSqlDao = dbi.onDemand(InvoiceItemSqlDao.class);
         this.nextBillingDatePoster = nextBillingDatePoster;
         this.tagUserApi = tagUserApi;
+        this.clock = clock;
     }
 
     @Override
@@ -507,35 +512,29 @@ public class DefaultInvoiceDao implements InvoiceDao {
     }
 
     private void notifyOfFutureBillingEvents(final InvoiceSqlDao dao, final UUID accountId, final int billCycleDay, final List<InvoiceItem> invoiceItems) {
-        DateTime nextBCD = null;
-        UUID subscriptionForNextBCD = null;
+        UUID subscriptionForNextNotification = null;
+        boolean shouldBeNotified = false;
         for (final InvoiceItem item : invoiceItems) {
             if (item.getInvoiceItemType() == InvoiceItemType.RECURRING) {
                 final RecurringInvoiceItem recurringInvoiceItem = (RecurringInvoiceItem) item;
                 if ((recurringInvoiceItem.getEndDate() != null) &&
-                        (recurringInvoiceItem.getAmount() == null ||
-                                recurringInvoiceItem.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
-                    if (nextBCD == null || nextBCD.compareTo(recurringInvoiceItem.getEndDate()) > 0) {
-                        nextBCD = recurringInvoiceItem.getEndDate();
-                        subscriptionForNextBCD = recurringInvoiceItem.getSubscriptionId();
-                    }
+                    (recurringInvoiceItem.getAmount() == null ||
+                     recurringInvoiceItem.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
+                    subscriptionForNextNotification = recurringInvoiceItem.getSubscriptionId();
+                    shouldBeNotified = true;
+                    break;
                 }
             }
         }
-        // We need to be notified if and only if the maximum end date of the invoiced recurring items is equal
-        // to the next bill cycle day.
-        // We take the maximum because we're guaranteed to have invoiced all subscriptions up until that date
-        // (and no further processing is needed).
-        // Also, we only need to get notified on the BDC. For other invoice events (e.g. phase changes),
+
+        // We only need to get notified on the BCD. For other invoice events (e.g. phase changes),
         // we'll be notified by entitlement.
-        if (subscriptionForNextBCD != null && nextBCD != null) {
-            final int lastDayOfMonth = nextBCD.dayOfMonth().withMaximumValue().getDayOfMonth();
-            final int nextBCDDay = nextBCD.getDayOfMonth();
-            // Small trick here in case the bill cycle day doesn't exist for that month, e.g. the bill cycle day
-            // is on the 31st, but the month has only 30 days
-            if (nextBCDDay == billCycleDay || (lastDayOfMonth == nextBCDDay && billCycleDay > lastDayOfMonth)) {
-                nextBillingDatePoster.insertNextBillingNotification(dao, accountId, subscriptionForNextBCD, nextBCD);
-            }
+        if (shouldBeNotified) {
+            // We could be notified at any time during the day at the billCycleDay - use the current time to
+            // spread the load
+            final DateTime nextNotificationDateTime = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(clock.getUTCNow(), billCycleDay);
+            // NextBillingDatePoster will ignore duplicates
+            nextBillingDatePoster.insertNextBillingNotification(dao, accountId, subscriptionForNextNotification, nextNotificationDateTime);
         }
     }
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/generator/InvoiceDateUtils.java b/invoice/src/main/java/com/ning/billing/invoice/generator/InvoiceDateUtils.java
index 5f7f86a..df130ee 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/generator/InvoiceDateUtils.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/generator/InvoiceDateUtils.java
@@ -18,6 +18,7 @@ package com.ning.billing.invoice.generator;
 
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
+import org.joda.time.MutableDateTime;
 
 public class InvoiceDateUtils {
     public static DateTime roundDateTimeToDate(final DateTime input, final DateTimeZone timeZone) {
@@ -28,4 +29,21 @@ public class InvoiceDateUtils {
 
         return new DateTime(tzAdjustedStartDate.getYear(), tzAdjustedStartDate.getMonthOfYear(), tzAdjustedStartDate.getDayOfMonth(), 0, 0, timeZone);
     }
+
+    public static DateTime calculateBillingCycleDateOnOrAfter(final DateTime date, final int billingCycleDay) {
+        final int lastDayOfMonth = date.dayOfMonth().getMaximumValue();
+
+        final MutableDateTime tmp = date.toMutableDateTime();
+        if (billingCycleDay > lastDayOfMonth) {
+            tmp.setDayOfMonth(lastDayOfMonth);
+        } else {
+            tmp.setDayOfMonth(billingCycleDay);
+        }
+        DateTime proposedDate = tmp.toDateTime();
+
+        while (proposedDate.isBefore(date)) {
+            proposedDate = proposedDate.plusMonths(1);
+        }
+        return proposedDate;
+    }
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java b/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
index 117df30..bf146cc 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/InAdvanceBillingMode.java
@@ -26,6 +26,7 @@ import org.joda.time.Months;
 import org.joda.time.MutableDateTime;
 
 import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.invoice.generator.InvoiceDateUtils;
 
 public class InAdvanceBillingMode implements BillingMode {
     private static final int ROUNDING_METHOD = InvoicingConfiguration.getRoundingMode();
@@ -49,7 +50,7 @@ public class InAdvanceBillingMode implements BillingMode {
         final List<RecurringInvoiceItemData> results = new ArrayList<RecurringInvoiceItemData>();
 
         // beginning from the start date, find the first billing date
-        final DateTime firstBillingCycleDate = calculateBillingCycleDateOnOrAfter(startDate, billingCycleDay);
+        final DateTime firstBillingCycleDate = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(startDate, billingCycleDay);
 
         // add pro-ration item if needed
         if (firstBillingCycleDate.isAfter(startDate)) {
@@ -92,7 +93,7 @@ public class InAdvanceBillingMode implements BillingMode {
         }
 
         // beginning from the start date, find the first billing date
-        final DateTime firstBillingCycleDate = calculateBillingCycleDateOnOrAfter(startDate, billingCycleDay);
+        final DateTime firstBillingCycleDate = InvoiceDateUtils.calculateBillingCycleDateOnOrAfter(startDate, billingCycleDay);
 
         // add pro-ration item if needed
         if (firstBillingCycleDate.isAfter(startDate)) {
@@ -124,23 +125,6 @@ public class InAdvanceBillingMode implements BillingMode {
         return results;
     }
 
-    private DateTime calculateBillingCycleDateOnOrAfter(final DateTime date, final int billingCycleDay) {
-        final int lastDayOfMonth = date.dayOfMonth().getMaximumValue();
-
-        final MutableDateTime tmp = date.toMutableDateTime();
-        if (billingCycleDay > lastDayOfMonth) {
-            tmp.setDayOfMonth(lastDayOfMonth);
-        } else {
-            tmp.setDayOfMonth(billingCycleDay);
-        }
-        DateTime proposedDate = tmp.toDateTime();
-
-        while (proposedDate.isBefore(date)) {
-            proposedDate = proposedDate.plusMonths(1);
-        }
-        return proposedDate;
-    }
-
     private BigDecimal calculateProRationBeforeFirstBillingPeriod(final DateTime startDate, final DateTime nextBillingCycleDate, final BillingPeriod billingPeriod) {
         final DateTime previousBillingCycleDate = nextBillingCycleDate.plusMonths(-billingPeriod.getNumberOfMonths());
 
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
index 78db034..e9385a2 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
@@ -19,13 +19,9 @@ package com.ning.billing.invoice.dao;
 import java.io.IOException;
 import java.net.URL;
 
-import org.skife.jdbi.v2.Handle;
 import org.skife.jdbi.v2.IDBI;
-import org.skife.jdbi.v2.TransactionCallback;
-import org.skife.jdbi.v2.TransactionStatus;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
 
 import com.ning.billing.KillbillTestSuiteWithEmbeddedDB;
 import com.ning.billing.config.InvoiceConfig;
@@ -114,7 +110,7 @@ public class InvoiceDaoTestBase extends InvoicingTestBase {
         final TagDefinitionDao tagDefinitionDao = new MockTagDefinitionDao();
         final TagDao tagDao = new AuditedTagDao(dbi, tagEventBuilder, bus);
         final TagUserApi tagUserApi = new DefaultTagUserApi(tagDefinitionDao, tagDao);
-        invoiceDao = new DefaultInvoiceDao(dbi, nextBillingDatePoster, tagUserApi);
+        invoiceDao = new DefaultInvoiceDao(dbi, nextBillingDatePoster, tagUserApi, clock);
         invoiceDao.test();
 
         invoiceItemSqlDao = dbi.onDemand(InvoiceItemSqlDao.class);
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java
index e0f3ea0..df2dd59 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/TestDefaultInvoiceDao.java
@@ -29,6 +29,7 @@ import com.ning.billing.invoice.InvoiceTestSuite;
 import com.ning.billing.invoice.notification.NextBillingDatePoster;
 import com.ning.billing.util.api.TagUserApi;
 import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.dao.ObjectType;
 import com.ning.billing.util.tag.ControlTagType;
 import com.ning.billing.util.tag.Tag;
@@ -49,7 +50,7 @@ public class TestDefaultInvoiceDao extends InvoiceTestSuite {
         final TagDefinitionDao tagDefinitionDao = new MockTagDefinitionDao();
         final TagDao tagDao = new MockTagDao();
         tagUserApi = new DefaultTagUserApi(tagDefinitionDao, tagDao);
-        dao = new DefaultInvoiceDao(idbi, poster, tagUserApi);
+        dao = new DefaultInvoiceDao(idbi, poster, tagUserApi, Mockito.mock(Clock.class));
     }
 
     @Test(groups = "fast")
diff --git a/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java b/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java
index 7989167..b0d75dc 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/tests/TestChargeBacks.java
@@ -87,7 +87,7 @@ public class TestChargeBacks extends InvoiceTestSuiteWithEmbeddedDB {
         final TagDefinitionDao tagDefinitionDao = new MockTagDefinitionDao();
         final TagDao tagDao = new MockTagDao();
         final TagUserApi tagUserApi = new DefaultTagUserApi(tagDefinitionDao, tagDao);
-        final InvoiceDao invoiceDao = new DefaultInvoiceDao(dbi, nextBillingDatePoster, tagUserApi);
+        final InvoiceDao invoiceDao = new DefaultInvoiceDao(dbi, nextBillingDatePoster, tagUserApi, clock);
         invoicePaymentApi = new DefaultInvoicePaymentApi(invoiceDao);
 
         context = new TestCallContext("Charge back tests");