killbill-memoizeit

Fixes #139 Yet another implementation for the convertion

12/5/2013 10:02:41 PM

Details

diff --git a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
index 506278a..2c94cb9 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
@@ -169,11 +169,11 @@ public class InvoiceDispatcher {
 
             // Make sure to first set the BCD if needed then get the account object (to have the BCD set)
             final BillingEventSet billingEvents = billingApi.getBillingEventsForAccountAndUpdateAccountBCD(accountId, context);
-            final Account account = accountApi.getAccountById(accountId, context);
 
+            final Account account = accountApi.getAccountById(accountId, context);
             final DateAndTimeZoneContext dateAndTimeZoneContext = billingEvents.iterator().hasNext() ?
                                                                   new DateAndTimeZoneContext(billingEvents.iterator().next().getEffectiveDate(), account.getTimeZone(), clock) :
-                                                                  new DateAndTimeZoneContext(null, account.getTimeZone(), clock);
+                                                                  null;
 
 
             List<Invoice> invoices = new ArrayList<Invoice>();
@@ -189,8 +189,8 @@ public class InvoiceDispatcher {
 
             final Currency targetCurrency = account.getCurrency();
 
-            final LocalDate targetDate = dateAndTimeZoneContext.computeTargetDate(targetDateTime);
-            final Invoice invoice = generator.generateInvoice(accountId, billingEvents, invoices, targetDate, targetCurrency);
+            final LocalDate targetDate = dateAndTimeZoneContext != null ? dateAndTimeZoneContext.computeTargetDate(targetDateTime) : null;
+            final Invoice invoice = targetDate != null ? generator.generateInvoice(accountId, billingEvents, invoices, targetDate, targetCurrency) : null;
             if (invoice == null) {
                 log.info("Generated null invoice for accountId {} and targetDate {} (targetDateTime {})", new Object[]{accountId, targetDate, targetDateTime});
                 if (!dryRun) {
diff --git a/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
index 03d7450..728bcce 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
@@ -194,7 +194,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         final LocalDate endDate = new LocalDate("2012-11-26");
 
 
-        ((ClockMock) clock).setTime(new DateTime(2012, 10, 26, 1, 12, 23, DateTimeZone.UTC));
+        ((ClockMock) clock).setTime(new DateTime(2012, 10, 13, 1, 12, 23, DateTimeZone.UTC));
 
         final DateAndTimeZoneContext dateAndTimeZoneContext = new DateAndTimeZoneContext(clock.getUTCNow(), DateTimeZone.forID("Pacific/Pitcairn"), clock);
 
@@ -217,10 +217,6 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         final LocalDate receivedTargetDate = new LocalDate(receivedDate, DateTimeZone.forID("Pacific/Pitcairn"));
         Assert.assertEquals(receivedTargetDate, endDate);
 
-        Assert.assertTrue(receivedDate.compareTo(new DateTime(2012, 11, 26, 9 /* 1 + 8 for Pitcairn */, 12, 23, DateTimeZone.UTC)) >= 0);
-        Assert.assertTrue(receivedDate.compareTo(new DateTime(2012, 11, 26, 9, 13, 0, DateTimeZone.UTC)) <= 0);
-
+        Assert.assertTrue(receivedDate.compareTo(new DateTime(2012, 11, 27, 1, 12, 23, DateTimeZone.UTC)) <= 0);
     }
-
-    //MDW add a test to cover when the account auto-invoice-off tag is present
 }
diff --git a/util/src/main/java/com/ning/billing/util/timezone/DateAndTimeZoneContext.java b/util/src/main/java/com/ning/billing/util/timezone/DateAndTimeZoneContext.java
index b9e60b1..c96a9f1 100644
--- a/util/src/main/java/com/ning/billing/util/timezone/DateAndTimeZoneContext.java
+++ b/util/src/main/java/com/ning/billing/util/timezone/DateAndTimeZoneContext.java
@@ -18,6 +18,7 @@ package com.ning.billing.util.timezone;
 
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
+import org.joda.time.Days;
 import org.joda.time.LocalDate;
 import org.joda.time.LocalTime;
 
@@ -31,6 +32,7 @@ import com.ning.billing.clock.Clock;
 public final class DateAndTimeZoneContext {
 
     private final LocalTime referenceTime;
+    private final int offsetFromUtc;
     private final DateTimeZone accountTimeZone;
     private final Clock clock;
 
@@ -38,6 +40,13 @@ public final class DateAndTimeZoneContext {
         this.clock = clock;
         this.referenceTime = effectiveDateTime != null ? effectiveDateTime.toLocalTime() : null;
         this.accountTimeZone = accountTimeZone;
+        this.offsetFromUtc = computeOffsetFromUtc(effectiveDateTime, accountTimeZone);
+    }
+
+    static int computeOffsetFromUtc(final DateTime effectiveDateTime, final DateTimeZone accountTimeZone) {
+        final LocalDate localDateInAccountTimeZone = new LocalDate(effectiveDateTime, accountTimeZone);
+        final LocalDate localDateInUTC = new LocalDate(effectiveDateTime, DateTimeZone.UTC);
+        return Days.daysBetween(localDateInUTC, localDateInAccountTimeZone).getDays();
     }
 
     public LocalDate computeTargetDate(final DateTime targetDateTime) {
@@ -50,9 +59,9 @@ public final class DateAndTimeZoneContext {
         // Since we create the targetDate for next invoice using the date from the notificationQ, we need to make sure
         // that this datetime once transformed into a LocalDate points to the correct day.
         //
-        // e.g If accountTimeZone is -8 and we want to invoice on the 16, with a toDateTimeAtCurrentTime = 00:00:23,
-        // we will generate a datetime that is 16T08:00:23 => LocalDate in that timeZone stays on the 16.
-        //
+        // All we need to do is figure is the transformation from DateTime (point in time) to LocalDate (date in account time zone)
+        // changed the day; if so, when we recompute a UTC date from LocalDate (date in account time zone), we can simply chose a reference
+        // time and apply the offset backward to end up on the right day
         //
         // We use clock.getUTCNow() to get the offset with account timezone but that may not be correct
         // when we transition from standard time and daylight saving time. We could end up with a result
@@ -60,9 +69,7 @@ public final class DateAndTimeZoneContext {
         // We will fix that by re-inserting ourselves in the notificationQ if we detect that there is no invoice
         // and yet the subscription is recurring and not cancelled.
         //
-        final int utcOffest = accountTimeZone.getOffset(clock.getUTCNow());
-        final int localToUTCOffest = -1 * utcOffest;
-        return invoiceItemEndDate.toDateTime(referenceTime, DateTimeZone.UTC).plusMillis(localToUTCOffest);
+        return invoiceItemEndDate.toDateTime(referenceTime, DateTimeZone.UTC).plusDays(-offsetFromUtc);
     }
 
     public DateTime computeUTCDateTimeFromNow() {
diff --git a/util/src/test/java/com/ning/billing/util/timezone/TestDateAndTimeZoneContext.java b/util/src/test/java/com/ning/billing/util/timezone/TestDateAndTimeZoneContext.java
new file mode 100644
index 0000000..f73a120
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/timezone/TestDateAndTimeZoneContext.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2010-2013 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.util.timezone;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.testng.annotations.Test;
+
+import com.ning.billing.util.UtilTestSuiteNoDB;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+//
+// There are two categories of tests, one that test the offset calculation and one that calculates
+// how to get a DateTime from a LocalDate (in account time zone)
+//
+// Tests {1, 2, 3} use an account timezone with a negative offset (-8) and tests {A, B, C} use an account timezone with a positive offset (+8)
+//
+public class TestDateAndTimeZoneContext extends UtilTestSuiteNoDB {
+
+    private final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTimeParser();
+
+    final String effectiveDateTime1 = "2012-01-20T07:30:42.000Z";
+    final String effectiveDateTime2 = "2012-01-20T08:00:00.000Z";
+    final String effectiveDateTime3 = "2012-01-20T08:45:33.000Z";
+
+    final String effectiveDateTimeA = "2012-01-20T16:30:42.000Z";
+    final String effectiveDateTimeB = "2012-01-20T16:00:00.000Z";
+    final String effectiveDateTimeC = "2012-01-20T15:30:42.000Z";
+
+
+    //
+    // Take an negative timezone offset and a reference time that is less than the offset (07:30:42 < 8)
+    // => to expect a negative offset of one day
+    //
+    @Test(groups = "fast")
+    public void testComputeOffset1() {
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime1);
+
+        int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+        assertEquals(offset, -1);
+    }
+
+    //
+    // Take an negative timezone offset and a reference time that is equal than the offset (08:00:00 = 8)
+    // => to expect an offset of 0
+    //
+    @Test(groups = "fast")
+    public void testComputeOffset2() {
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime2);
+
+        int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+        assertEquals(offset, 0);
+    }
+
+    //
+    // Take an negative timezone offset and a reference time that is greater than the offset (08:45:33 > 8)
+    // => to expect an offset of 0
+    //
+    @Test(groups = "fast")
+    public void testComputeOffset3() {
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime3);
+
+        int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+        assertEquals(offset, 0);
+    }
+
+    //
+    // Take an positive timezone offset and a reference time that closer to the end of the day than the timezone (16:30:42 + 8 > 24)
+    // => to expect a positive offset of one day
+    //
+    @Test(groups = "fast")
+    public void testComputeOffsetA() {
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeA);
+
+        int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+        assertEquals(offset, 1);
+    }
+
+    //
+    // Take an positive timezone offset and a reference time that brings us exactly at the end of the day (16:00:00 + 8 = 24)
+    // => to expect an offset of 1
+    //
+    @Test(groups = "fast")
+    public void testComputeOffsetB() {
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeB);
+
+        int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+        assertEquals(offset, 1);
+    }
+
+    //
+    // Take an positive timezone offset and a reference time that further away to the end of the day  (15:30:42 + 8 < 24)
+    // =>  to expect an offset of 0
+    //
+    @Test(groups = "fast")
+    public void testComputeOffsetC() {
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeC);
+
+        int offset = DateAndTimeZoneContext.computeOffsetFromUtc(effectiveDateTime, timeZone);
+        assertEquals(offset, 0);
+    }
+
+    @Test(groups = "fast")
+    public void testComputeUTCDateTimeFromLocalDate1() {
+
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime1);
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+        final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+        final LocalDate endDate = new LocalDate(2013, 01, 19);
+        final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+        assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+    }
+
+
+    @Test(groups = "fast")
+    public void testComputeUTCDateTimeFromLocalDate2() {
+
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime2);
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+        final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+        final LocalDate endDate = new LocalDate(2013, 01, 20);
+        final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+        assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+    }
+
+
+    @Test(groups = "fast")
+    public void testComputeUTCDateTimeFromLocalDate3() {
+
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTime3);
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(-8);
+        final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+        final LocalDate endDate = new LocalDate(2013, 01, 20);
+        final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+        assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+    }
+
+    @Test(groups = "fast")
+    public void testComputeUTCDateTimeFromLocalDateA() {
+
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeA);
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+        final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+        final LocalDate endDate = new LocalDate(2013, 01, 21);
+        final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+        assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+    }
+
+    @Test(groups = "fast")
+    public void testComputeUTCDateTimeFromLocalDateB() {
+
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeB);
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+        final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+        final LocalDate endDate = new LocalDate(2013, 01, 21);
+        final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+        assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+    }
+
+    @Test(groups = "fast")
+    public void testComputeUTCDateTimeFromLocalDateC() {
+
+        final DateTime effectiveDateTime = DATE_TIME_FORMATTER.parseDateTime(effectiveDateTimeC);
+
+        final DateTimeZone timeZone = DateTimeZone.forOffsetHours(8);
+        final DateAndTimeZoneContext dateContext = new DateAndTimeZoneContext(effectiveDateTime, timeZone, clock);
+
+        final LocalDate endDate = new LocalDate(2013, 01, 20);
+        final DateTime endDateTimeInUTC = dateContext.computeUTCDateTimeFromLocalDate(endDate);
+        assertTrue(endDateTimeInUTC.compareTo(effectiveDateTime.plusYears(1)) == 0);
+    }
+}