Repository: killbill/killbill
Issue: Wrong computed invoice target date for non-UTC TZ

See [mailing-list thread](https://groups.google.com/forum/#!msg/killbilling-users/ChJ7DeOQCzk/BQzCL5E0BAAJ).

Changed files:
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceOffTag.java
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index a7140c378f..0e8cd1a603 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -426,15 +426,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
         return (DefaultSubscriptionBase) sub;
     }
 
-    protected Account createAccountWithOsgiPaymentMethod(final AccountData accountData) throws Exception {
-        return createAccountWithPaymentMethod(accountData, BeatrixIntegrationModule.OSGI_PLUGIN_NAME);
-    }
-
     protected Account createAccountWithNonOsgiPaymentMethod(final AccountData accountData) throws Exception {
-        return createAccountWithPaymentMethod(accountData, BeatrixIntegrationModule.NON_OSGI_PLUGIN_NAME);
-    }
-
-    private Account createAccountWithPaymentMethod(final AccountData accountData, final String paymentPluginName) throws Exception {
         final Account account = accountUserApi.createAccount(accountData, callContext);
         assertNotNull(account);
 
@@ -442,7 +434,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
 
         final PaymentMethodPlugin info = createPaymentMethodPlugin();
 
-        paymentApi.addPaymentMethod(account, UUID.randomUUID().toString(), paymentPluginName, true, info, PLUGIN_PROPERTIES, callContext);
+        paymentApi.addPaymentMethod(account, UUID.randomUUID().toString(), BeatrixIntegrationModule.NON_OSGI_PLUGIN_NAME, true, info, PLUGIN_PROPERTIES, callContext);
         return accountUserApi.getAccountById(account.getId(), callContext);
     }
 
@@ -451,6 +443,10 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
     }
 
     protected AccountData getAccountData(@Nullable final Integer billingDay) {
+        return getAccountData(billingDay, DateTimeZone.UTC);
+    }
+
+    protected AccountData getAccountData(@Nullable final Integer billingDay, final DateTimeZone tz) {
         final MockAccountBuilder builder = new MockAccountBuilder()
                 .name(UUID.randomUUID().toString().substring(1, 8))
                 .firstNameLength(6)
@@ -460,7 +456,7 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB implemen
                 .externalKey(UUID.randomUUID().toString().substring(1, 8))
                 .currency(Currency.USD)
                 .referenceTime(clock.getUTCNow())
-                .timeZone(DateTimeZone.UTC);
+                .timeZone(tz);
         if (billingDay != null) {
             builder.billingCycleDayLocal(billingDay);
         }
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceOffTag.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceOffTag.java
index 3d5c66a248..455173d6ee 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceOffTag.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoInvoiceOffTag.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -20,9 +20,10 @@ package org.killbill.billing.beatrix.integration;
 
 import java.util.Collection;
 import java.util.List;
-import java.util.UUID;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.api.TestApiListener.NextEvent;
@@ -30,13 +31,8 @@ import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
 import org.killbill.billing.invoice.api.Invoice;
-import org.killbill.billing.invoice.api.InvoiceStatus;
 import org.killbill.billing.invoice.api.InvoiceUserApi;
-import org.killbill.billing.util.api.TagApiException;
-import org.killbill.billing.util.api.TagDefinitionApiException;
 import org.killbill.billing.util.api.TagUserApi;
-import org.killbill.billing.util.tag.ControlTagType;
-import org.killbill.billing.util.tag.Tag;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
@@ -102,4 +98,47 @@ public class TestIntegrationWithAutoInvoiceOffTag extends TestIntegrationBase {
         assertEquals(invoices.size(), 1);
     }
 
+    @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/1074")
+    public void testAutoInvoiceOffWithTZ() throws Exception {
+        clock.setTime(new DateTime(2018, 12, 1, 0, 25, 0, 0));
+
+        account = createAccountWithNonOsgiPaymentMethod(getAccountData(null, DateTimeZone.forID("America/Los_Angeles")));
+        assertEquals(account.getBillCycleDayLocal(), (Integer) 0);
+
+        final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE, term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        assertNotNull(bpEntitlement);
+        assertEquals(accountUserApi.getAccountById(account.getId(), callContext).getBillCycleDayLocal(), (Integer) 30);
+
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext);
+        assertEquals(invoices.size(), 1);
+        assertEquals(invoices.get(0).getInvoiceItems().size(), 1);
+        assertEquals(invoices.get(0).getTargetDate(), new LocalDate(2018, 11, 30));
+        assertEquals(invoices.get(0).getInvoiceItems().get(0).getStartDate(), new LocalDate(2018, 11, 30));
+
+        clock.setTime(new DateTime(2018, 12, 30, 0, 20, 0, 0));
+        assertEquals(clock.getUTCToday(), new LocalDate(2018, 12, 30));
+        assertEquals(clock.getUTCNow().toDateTime(account.getTimeZone()).toLocalDate(), new LocalDate(2018, 12, 29));
+        // Still in trial
+        assertListenerStatus();
+        assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getName(), "shotgun-monthly-trial");
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext).size(), 1);
+
+        // Adding / Removing AUTO_INVOICING_OFF shouldn't have any impact
+        add_AUTO_INVOICING_OFF_Tag(account.getId(), ObjectType.ACCOUNT);
+        remove_AUTO_INVOICING_OFF_Tag(account.getId(), ObjectType.ACCOUNT, NextEvent.NULL_INVOICE);
+
+        assertListenerStatus();
+        assertEquals(entitlementApi.getEntitlementForId(bpEntitlement.getId(), callContext).getLastActivePhase().getName(), "shotgun-monthly-trial");
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext).size(), 1);
+
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        clock.setTime(new DateTime(2018, 12, 31, 0, 25, 0, 0));
+        assertListenerStatus();
+
+        invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, false, callContext);
+        assertEquals(invoices.size(), 2);
+        assertEquals(invoices.get(1).getTargetDate(), new LocalDate(2018, 12, 30));
+        assertEquals(invoices.get(1).getInvoiceItems().get(0).getStartDate(), new LocalDate(2018, 12, 30));
+        assertEquals(invoices.get(1).getInvoiceItems().get(0).getEndDate(), new LocalDate(2019, 1, 30));
+    }
 }
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java
index f901875e05..4715f25131 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithAutoPayOff.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -22,21 +22,15 @@ import java.math.BigDecimal;
 import java.util.Collection;
 
 import org.joda.time.DateTime;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
 import org.killbill.billing.invoice.api.Invoice;
-import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
-import org.killbill.billing.util.config.definition.PaymentConfig;
-
-import com.google.inject.Inject;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
@@ -45,10 +39,8 @@ import static org.testng.Assert.assertTrue;
 public class TestIntegrationWithAutoPayOff extends TestIntegrationBase {
 
     private Account account;
-    private SubscriptionBaseBundle bundle;
     private String productName;
     private BillingPeriod term;
-    private String planSetName;
 
     @Override
     @BeforeMethod(groups = "slow")
@@ -62,7 +54,6 @@ public class TestIntegrationWithAutoPayOff extends TestIntegrationBase {
         assertNotNull(account);
         productName = "Shotgun";
         term = BillingPeriod.MONTHLY;
-        planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
     }
 
     @Test(groups = "slow")
@@ -242,7 +233,6 @@ public class TestIntegrationWithAutoPayOff extends TestIntegrationBase {
 
     }
 
-
     private void addDelayBceauseOfLackOfCorrectSynchro() {
         // TODO When removing the tag, the payment system will schedule retries for payments that are in non terminal state
         // The issue is that at this point we know the event went on the bus but we don't know if the listener in payment completed
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index 767cd30d00..2bc350b107 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -326,7 +326,7 @@ public class InvoiceDispatcher {
         LocalDate inputTargetDate = inputTargetDateMaybeNull;
         // A null inputTargetDate is only allowed in UPCOMING_INVOICE dryRun mode to have the system compute it
         if (inputTargetDate == null && !upcomingInvoiceDryRun) {
-            inputTargetDate = clock.getUTCToday();
+            inputTargetDate = context.toLocalDate(clock.getUTCNow());
         }
         Preconditions.checkArgument(inputTargetDate != null || upcomingInvoiceDryRun, "inputTargetDate is required in non dryRun mode");