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