killbill-memoizeit

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithEntilementPlugin.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithEntilementPlugin.java
index f6037c5..1fb592d 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithEntilementPlugin.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithEntilementPlugin.java
@@ -49,6 +49,7 @@ import org.killbill.billing.osgi.api.OSGIServiceDescriptor;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -185,6 +186,10 @@ public class TestWithEntilementPlugin extends TestIntegrationBase {
 
         @Override
         public OnSuccessEntitlementResult onSuccessCall(final EntitlementContext entitlementContext, final Iterable<PluginProperty> properties) throws EntitlementPluginApiException {
+            for (final BaseEntitlementWithAddOnsSpecifier specifier : entitlementContext.getBaseEntitlementWithAddOnsSpecifiers()) {
+                Assert.assertNotNull(specifier.getBundleId());
+                Assert.assertNotNull(specifier.getExternalKey());
+            }
             return null;
         }
 
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultBaseEntitlementWithAddOnsSpecifier.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultBaseEntitlementWithAddOnsSpecifier.java
index 265d0b9..6332460 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultBaseEntitlementWithAddOnsSpecifier.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultBaseEntitlementWithAddOnsSpecifier.java
@@ -1,5 +1,6 @@
 /*
- * Copyright 2016 The Billing Project, LLC
+ * Copyright 2014-2019 Groupon, Inc
+ * Copyright 2014-2019 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,13 +23,24 @@ import org.joda.time.LocalDate;
 
 public class DefaultBaseEntitlementWithAddOnsSpecifier implements BaseEntitlementWithAddOnsSpecifier {
 
-    private final UUID bundleId;
-    private final String externalKey;
     private final Iterable<EntitlementSpecifier> entitlementSpecifier;
     private final LocalDate entitlementEffectiveDate;
     private final LocalDate billingEffectiveDate;
     private final boolean isMigrated;
 
+    // Maybe populated after create or transfer
+    private UUID bundleId;
+    private String externalKey;
+
+    public DefaultBaseEntitlementWithAddOnsSpecifier(final BaseEntitlementWithAddOnsSpecifier input) {
+        this(input.getBundleId(),
+             input.getExternalKey(),
+             input.getEntitlementSpecifier(),
+             input.getEntitlementEffectiveDate(),
+             input.getBillingEffectiveDate(),
+             input.isMigrated());
+    }
+
     public DefaultBaseEntitlementWithAddOnsSpecifier(final UUID bundleId,
                                                      final String externalKey,
                                                      final Iterable<EntitlementSpecifier> entitlementSpecifier,
@@ -48,11 +60,19 @@ public class DefaultBaseEntitlementWithAddOnsSpecifier implements BaseEntitlemen
         return bundleId;
     }
 
+    public void setBundleId(final UUID bundleId) {
+        this.bundleId = bundleId;
+    }
+
     @Override
     public String getExternalKey() {
         return externalKey;
     }
 
+    public void setExternalKey(final String externalKey) {
+        this.externalKey = externalKey;
+    }
+
     @Override
     public Iterable<EntitlementSpecifier> getEntitlementSpecifier() {
         return entitlementSpecifier;
@@ -72,4 +92,57 @@ public class DefaultBaseEntitlementWithAddOnsSpecifier implements BaseEntitlemen
     public boolean isMigrated() {
         return isMigrated;
     }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("DefaultBaseEntitlementWithAddOnsSpecifier{");
+        sb.append("entitlementSpecifier=").append(entitlementSpecifier);
+        sb.append(", entitlementEffectiveDate=").append(entitlementEffectiveDate);
+        sb.append(", billingEffectiveDate=").append(billingEffectiveDate);
+        sb.append(", isMigrated=").append(isMigrated);
+        sb.append(", bundleId=").append(bundleId);
+        sb.append(", externalKey='").append(externalKey).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final DefaultBaseEntitlementWithAddOnsSpecifier that = (DefaultBaseEntitlementWithAddOnsSpecifier) o;
+
+        if (isMigrated != that.isMigrated) {
+            return false;
+        }
+        if (entitlementSpecifier != null ? !entitlementSpecifier.equals(that.entitlementSpecifier) : that.entitlementSpecifier != null) {
+            return false;
+        }
+        if (entitlementEffectiveDate != null ? entitlementEffectiveDate.compareTo(that.entitlementEffectiveDate) != 0 : that.entitlementEffectiveDate != null) {
+            return false;
+        }
+        if (billingEffectiveDate != null ? billingEffectiveDate.compareTo(that.billingEffectiveDate) != 0 : that.billingEffectiveDate != null) {
+            return false;
+        }
+        if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+            return false;
+        }
+        return externalKey != null ? externalKey.equals(that.externalKey) : that.externalKey == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = entitlementSpecifier != null ? entitlementSpecifier.hashCode() : 0;
+        result = 31 * result + (entitlementEffectiveDate != null ? entitlementEffectiveDate.hashCode() : 0);
+        result = 31 * result + (billingEffectiveDate != null ? billingEffectiveDate.hashCode() : 0);
+        result = 31 * result + (isMigrated ? 1 : 0);
+        result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+        result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+        return result;
+    }
 }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
index 4e3200b..794cc6b 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2018 Groupon, Inc
- * Copyright 2014-2018 The Billing Project, LLC
+ * Copyright 2014-2019 Groupon, Inc
+ * Copyright 2014-2019 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
@@ -336,7 +336,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
         final WithEntitlementPlugin<Entitlement> cancelEntitlementWithPlugin = new WithEntitlementPlugin<Entitlement>() {
 
             @Override
-            public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Entitlement doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
                 if (eventsStream.isEntitlementCancelled()) {
                     throw new EntitlementApiException(ErrorCode.SUB_CANCEL_BAD_STATE, getId(), EntitlementState.CANCELLED);
                 }
@@ -402,7 +402,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
         final WithEntitlementPlugin<Void> uncancelEntitlementWithPlugin = new WithEntitlementPlugin<Void>() {
 
             @Override
-            public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Void doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
                 if (eventsStream.isSubscriptionCancelled()) {
                     throw new EntitlementApiException(ErrorCode.SUB_UNCANCEL_BAD_STATE, getId());
                 }
@@ -484,7 +484,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
         final WithEntitlementPlugin<Entitlement> cancelEntitlementWithPlugin = new WithEntitlementPlugin<Entitlement>() {
             @Override
-            public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Entitlement doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
                 if (eventsStream.isEntitlementCancelled()) {
                     throw new EntitlementApiException(ErrorCode.SUB_CANCEL_BAD_STATE, getId(), EntitlementState.CANCELLED);
                 }
@@ -566,7 +566,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
         final WithEntitlementPlugin<Entitlement> changePlanWithPlugin = new WithEntitlementPlugin<Entitlement>() {
             @Override
-            public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Entitlement doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
                 if (!eventsStream.isEntitlementActive()) {
                     throw new EntitlementApiException(ErrorCode.SUB_CHANGE_NON_ACTIVE, getId(), getState());
                 }
@@ -638,7 +638,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
         final WithEntitlementPlugin<Void> undoChangePlanEntitlementWithPlugin = new WithEntitlementPlugin<Void>() {
 
             @Override
-            public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Void doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
 
                 try {
                     getSubscriptionBase().undoChangePlan(callContext);
@@ -682,7 +682,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
         final WithEntitlementPlugin<Entitlement> changePlanWithPlugin = new WithEntitlementPlugin<Entitlement>() {
             @Override
-            public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Entitlement doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
 
                 if (effectiveDate != null && effectiveDate.compareTo(eventsStream.getEntitlementEffectiveStartDate()) < 0) {
                     throw new EntitlementApiException(ErrorCode.SUB_CHANGE_NON_ACTIVE, getId(), getState());
@@ -757,7 +757,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
         final WithEntitlementPlugin<Entitlement> changePlanWithPlugin = new WithEntitlementPlugin<Entitlement>() {
             @Override
-            public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Entitlement doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
 
 
                 final InternalCallContext context = internalCallContextFactory.createInternalCallContext(getAccountId(), callContext);
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
index 3deb24d..ea40e75 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2018 Groupon, Inc
- * Copyright 2014-2018 The Billing Project, LLC
+ * Copyright 2014-2019 Groupon, Inc
+ * Copyright 2014-2019 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
@@ -306,7 +306,7 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
 
         final WithEntitlementPlugin<UUID> transferWithPlugin = new WithEntitlementPlugin<UUID>() {
             @Override
-            public UUID doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public UUID doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
                 final boolean cancelImm;
                 switch (billingPolicy) {
                     case IMMEDIATE:
@@ -332,11 +332,14 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
                         throw new EntitlementApiException(new SubscriptionBaseApiException(ErrorCode.SUB_GET_INVALID_BUNDLE_KEY, externalKey));
                     }
 
-                    final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = getFirstBaseEntitlementWithAddOnsSpecifier(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers());
+                    final DefaultBaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = getFirstBaseEntitlementWithAddOnsSpecifier(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers());
 
                     final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getBillingEffectiveDate(), updatedPluginContext.getCreatedDate(), contextWithSourceAccountRecordId);
                     final SubscriptionBaseBundle newBundle = subscriptionBaseTransferApi.transferBundle(sourceAccountId, destAccountId, externalKey, requestedDate, true, cancelImm, context);
 
+                    // Update the context for plugins
+                    baseEntitlementWithAddOnsSpecifier.setBundleId(newBundle.getId());
+                    baseEntitlementWithAddOnsSpecifier.setExternalKey(newBundle.getExternalKey());
 
                     final Map<BlockingState, UUID> blockingStates = new HashMap<BlockingState, UUID>();
 
@@ -359,7 +362,6 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
                     }
                     entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(blockingStates, contextWithDestAccountRecordId);
 
-
                     return newBundle.getId();
                 } catch (final SubscriptionBaseTransferApiException e) {
                     throw new EntitlementApiException(e);
@@ -389,7 +391,7 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
 
         final WithEntitlementPlugin<List<UUID>> createBaseEntitlementsWithAddOns = new WithEntitlementPlugin<List<UUID>>() {
             @Override
-            public List<UUID> doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public List<UUID> doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
                 final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
 
                 final Catalog catalog;
@@ -446,13 +448,20 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
                     throw new EntitlementApiException(e);
                 }
 
+                // Update the context for plugins (assume underlying ordering is respected)
+                for (int i = 0; i < subscriptionsWithAddOns.size(); i++) {
+                    final SubscriptionBaseWithAddOns subscriptionBaseWithAddOns = subscriptionsWithAddOns.get(i);
+                    updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers(i).setBundleId(subscriptionBaseWithAddOns.getBundle().getId());
+                    updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers(i).setExternalKey(subscriptionBaseWithAddOns.getBundle().getExternalKey());
+                }
+
                 return createEntitlementEvents(baseEntitlementWithAddOnsSpecifiersAfterPlugins, subscriptionsWithAddOns, updatedPluginContext, contextWithValidAccountRecordId);
             }
         };
         return pluginExecution.executeWithPlugin(createBaseEntitlementsWithAddOns, pluginContext);
     }
 
-    private BaseEntitlementWithAddOnsSpecifier getFirstBaseEntitlementWithAddOnsSpecifier(final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers) throws SubscriptionBaseApiException {
+    private DefaultBaseEntitlementWithAddOnsSpecifier getFirstBaseEntitlementWithAddOnsSpecifier(final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers) throws SubscriptionBaseApiException {
         if (baseEntitlementWithAddOnsSpecifiers == null) {
             throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
         }
@@ -462,7 +471,7 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
             throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
         }
 
-        return iterator.next();
+        return (DefaultBaseEntitlementWithAddOnsSpecifier) iterator.next();
     }
 
     private UUID populateCaches(final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier,
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
index 03811c9..c5ce58a 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2019 Groupon, Inc
+ * Copyright 2014-2019 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
@@ -17,6 +17,7 @@
 
 package org.killbill.billing.entitlement.api;
 
+import java.util.List;
 import java.util.UUID;
 
 import javax.annotation.Nullable;
@@ -31,6 +32,8 @@ import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.CallOrigin;
 import org.killbill.billing.util.callcontext.UserType;
 
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 
 public class DefaultEntitlementContext implements EntitlementContext {
@@ -38,7 +41,7 @@ public class DefaultEntitlementContext implements EntitlementContext {
     private final OperationType operationType;
     private final UUID accountId;
     private final UUID destinationAccountId;
-    private final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers;
+    private final List<DefaultBaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers;
     private final BillingActionPolicy billingActionPolicy;
     private final Iterable<PluginProperty> pluginProperties;
     private final UUID userToken;
@@ -56,26 +59,16 @@ public class DefaultEntitlementContext implements EntitlementContext {
         this(prev.getOperationType(),
              prev.getAccountId(),
              prev.getDestinationAccountId(),
-             pluginResult != null ? merge(prev.getBaseEntitlementWithAddOnsSpecifiers(), pluginResult.getAdjustedBaseEntitlementWithAddOnsSpecifiers()) : prev.getBaseEntitlementWithAddOnsSpecifiers(),
+             pluginResult != null ? lastUnlessEmptyOrFirst(prev.getBaseEntitlementWithAddOnsSpecifiers(), pluginResult.getAdjustedBaseEntitlementWithAddOnsSpecifiers()) : prev.getBaseEntitlementWithAddOnsSpecifiers(),
              pluginResult != null && pluginResult.getAdjustedBillingActionPolicy() != null ? pluginResult.getAdjustedBillingActionPolicy() : prev.getBillingActionPolicy(),
-             pluginResult != null ? merge(prev.getPluginProperties(), pluginResult.getAdjustedPluginProperties()) : prev.getPluginProperties(),
+             pluginResult != null ? lastUnlessEmptyOrFirst(prev.getPluginProperties(), pluginResult.getAdjustedPluginProperties()) : prev.getPluginProperties(),
              prev);
     }
 
-    private static <T> Iterable<T> merge(final Iterable<T> prevValues, final Iterable<T> newValues) {
-        // Be lenient if a plugin returns an empty list (default behavior for Ruby plugins): at this point,
-        // we know the isAborted flag hasn't been set, so let's assume the user actually wants to use the previous list
-        if (newValues != null && !Iterables.isEmpty(newValues)) {
-            return newValues;
-        } else {
-            return prevValues;
-        }
-    }
-
     public DefaultEntitlementContext(final OperationType operationType,
                                      final UUID accountId,
                                      final UUID destinationAccountId,
-                                     final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers,
+                                     @Nullable final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers,
                                      @Nullable final BillingActionPolicy actionPolicy,
                                      final Iterable<PluginProperty> pluginProperties,
                                      final CallContext callContext) {
@@ -84,11 +77,10 @@ public class DefaultEntitlementContext implements EntitlementContext {
              callContext.getComments(), callContext.getCreatedDate(), callContext.getUpdatedDate(), callContext.getTenantId());
     }
 
-
     public DefaultEntitlementContext(final OperationType operationType,
                                      final UUID accountId,
                                      final UUID destinationAccountId,
-                                     final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers,
+                                     @Nullable final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers,
                                      @Nullable final BillingActionPolicy actionPolicy,
                                      final Iterable<PluginProperty> pluginProperties,
                                      final UUID userToken,
@@ -103,7 +95,17 @@ public class DefaultEntitlementContext implements EntitlementContext {
         this.operationType = operationType;
         this.accountId = accountId;
         this.destinationAccountId = destinationAccountId;
-        this.baseEntitlementWithAddOnsSpecifiers = baseEntitlementWithAddOnsSpecifiers;
+        if (baseEntitlementWithAddOnsSpecifiers == null) {
+            this.baseEntitlementWithAddOnsSpecifiers = ImmutableList.<DefaultBaseEntitlementWithAddOnsSpecifier>of();
+        } else {
+            this.baseEntitlementWithAddOnsSpecifiers = ImmutableList.<DefaultBaseEntitlementWithAddOnsSpecifier>copyOf(Iterables.<BaseEntitlementWithAddOnsSpecifier, DefaultBaseEntitlementWithAddOnsSpecifier>transform(baseEntitlementWithAddOnsSpecifiers,
+                                                                                                                                                                                                                          new Function<BaseEntitlementWithAddOnsSpecifier, DefaultBaseEntitlementWithAddOnsSpecifier>() {
+                                                                                                                                                                                                                              @Override
+                                                                                                                                                                                                                              public DefaultBaseEntitlementWithAddOnsSpecifier apply(final BaseEntitlementWithAddOnsSpecifier input) {
+                                                                                                                                                                                                                                  return new DefaultBaseEntitlementWithAddOnsSpecifier(input);
+                                                                                                                                                                                                                              }
+                                                                                                                                                                                                                          }));
+        }
         this.billingActionPolicy = actionPolicy;
         this.pluginProperties = pluginProperties;
         this.userToken = userToken;
@@ -117,6 +119,16 @@ public class DefaultEntitlementContext implements EntitlementContext {
         this.tenantId = tenantId;
     }
 
+    private static <T> Iterable<T> lastUnlessEmptyOrFirst(final Iterable<T> prevValues, final Iterable<T> newValues) {
+        // Be lenient if a plugin returns an empty list (default behavior for Ruby plugins): at this point,
+        // we know the isAborted flag hasn't been set, so let's assume the user actually wants to use the previous list
+        if (newValues != null && !Iterables.isEmpty(newValues)) {
+            return newValues;
+        } else {
+            return prevValues;
+        }
+    }
+
     @Override
     public OperationType getOperationType() {
         return operationType;
@@ -134,7 +146,12 @@ public class DefaultEntitlementContext implements EntitlementContext {
 
     @Override
     public Iterable<BaseEntitlementWithAddOnsSpecifier> getBaseEntitlementWithAddOnsSpecifiers() {
-        return baseEntitlementWithAddOnsSpecifiers;
+        //noinspection unchecked
+        return (Iterable) baseEntitlementWithAddOnsSpecifiers;
+    }
+
+    public DefaultBaseEntitlementWithAddOnsSpecifier getBaseEntitlementWithAddOnsSpecifiers(final int idx) {
+        return baseEntitlementWithAddOnsSpecifiers.get(idx);
     }
 
     @Override
@@ -191,4 +208,102 @@ public class DefaultEntitlementContext implements EntitlementContext {
     public UUID getTenantId() {
         return tenantId;
     }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("DefaultEntitlementContext{");
+        sb.append("operationType=").append(operationType);
+        sb.append(", accountId=").append(accountId);
+        sb.append(", destinationAccountId=").append(destinationAccountId);
+        sb.append(", baseEntitlementWithAddOnsSpecifiers=").append(baseEntitlementWithAddOnsSpecifiers);
+        sb.append(", billingActionPolicy=").append(billingActionPolicy);
+        sb.append(", pluginProperties=").append(pluginProperties);
+        sb.append(", userToken=").append(userToken);
+        sb.append(", userName='").append(userName).append('\'');
+        sb.append(", callOrigin=").append(callOrigin);
+        sb.append(", userType=").append(userType);
+        sb.append(", reasonCode='").append(reasonCode).append('\'');
+        sb.append(", comments='").append(comments).append('\'');
+        sb.append(", createdDate=").append(createdDate);
+        sb.append(", updatedDate=").append(updatedDate);
+        sb.append(", tenantId=").append(tenantId);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final DefaultEntitlementContext that = (DefaultEntitlementContext) o;
+
+        if (operationType != that.operationType) {
+            return false;
+        }
+        if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+            return false;
+        }
+        if (destinationAccountId != null ? !destinationAccountId.equals(that.destinationAccountId) : that.destinationAccountId != null) {
+            return false;
+        }
+        if (baseEntitlementWithAddOnsSpecifiers != null ? !baseEntitlementWithAddOnsSpecifiers.equals(that.baseEntitlementWithAddOnsSpecifiers) : that.baseEntitlementWithAddOnsSpecifiers != null) {
+            return false;
+        }
+        if (billingActionPolicy != that.billingActionPolicy) {
+            return false;
+        }
+        if (pluginProperties != null ? !pluginProperties.equals(that.pluginProperties) : that.pluginProperties != null) {
+            return false;
+        }
+        if (userToken != null ? !userToken.equals(that.userToken) : that.userToken != null) {
+            return false;
+        }
+        if (userName != null ? !userName.equals(that.userName) : that.userName != null) {
+            return false;
+        }
+        if (callOrigin != that.callOrigin) {
+            return false;
+        }
+        if (userType != that.userType) {
+            return false;
+        }
+        if (reasonCode != null ? !reasonCode.equals(that.reasonCode) : that.reasonCode != null) {
+            return false;
+        }
+        if (comments != null ? !comments.equals(that.comments) : that.comments != null) {
+            return false;
+        }
+        if (createdDate != null ? createdDate.compareTo(that.createdDate) != 0 : that.createdDate != null) {
+            return false;
+        }
+        if (updatedDate != null ? updatedDate.compareTo(that.updatedDate) != 0 : that.updatedDate != null) {
+            return false;
+        }
+        return tenantId != null ? tenantId.equals(that.tenantId) : that.tenantId == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = operationType != null ? operationType.hashCode() : 0;
+        result = 31 * result + (accountId != null ? accountId.hashCode() : 0);
+        result = 31 * result + (destinationAccountId != null ? destinationAccountId.hashCode() : 0);
+        result = 31 * result + (baseEntitlementWithAddOnsSpecifiers != null ? baseEntitlementWithAddOnsSpecifiers.hashCode() : 0);
+        result = 31 * result + (billingActionPolicy != null ? billingActionPolicy.hashCode() : 0);
+        result = 31 * result + (pluginProperties != null ? pluginProperties.hashCode() : 0);
+        result = 31 * result + (userToken != null ? userToken.hashCode() : 0);
+        result = 31 * result + (userName != null ? userName.hashCode() : 0);
+        result = 31 * result + (callOrigin != null ? callOrigin.hashCode() : 0);
+        result = 31 * result + (userType != null ? userType.hashCode() : 0);
+        result = 31 * result + (reasonCode != null ? reasonCode.hashCode() : 0);
+        result = 31 * result + (comments != null ? comments.hashCode() : 0);
+        result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+        result = 31 * result + (updatedDate != null ? updatedDate.hashCode() : 0);
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        return result;
+    }
 }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionApi.java
index 71ffea4..09d8f4c 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionApi.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2018 Groupon, Inc
- * Copyright 2014-2018 The Billing Project, LLC
+ * Copyright 2014-2019 Groupon, Inc
+ * Copyright 2014-2019 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
@@ -313,7 +313,7 @@ public class DefaultSubscriptionApi implements SubscriptionApi {
             final InternalCallContext internalCallContextWithValidAccountId = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
 
             @Override
-            public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Void doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
                 subscriptionBaseInternalApi.updateExternalKey(bundleId, newExternalKey, internalCallContextWithValidAccountId);
                 return null;
             }
@@ -401,7 +401,7 @@ public class DefaultSubscriptionApi implements SubscriptionApi {
         final WithEntitlementPlugin<Void> addBlockingStateWithPlugin = new WithEntitlementPlugin<Void>() {
 
             @Override
-            public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Void doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
                 entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(blockingState, internalCallContextWithValidAccountId);
                 return null;
             }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java
index 0596c73..45e427b 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2019 Groupon, Inc
+ * Copyright 2014-2019 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
@@ -42,7 +42,7 @@ public class EntitlementPluginExecution {
     private final OSGIServiceRegistration<EntitlementPluginApi> pluginRegistry;
 
     public interface WithEntitlementPlugin<T> {
-        T doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException;
+        T doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException;
     }
 
     @Inject
@@ -52,7 +52,7 @@ public class EntitlementPluginExecution {
     }
 
     public void executeWithPlugin(final Callable<Void> preCallbacksCallback, final List<WithEntitlementPlugin> callbacks, final Iterable<EntitlementContext> pluginContexts) throws EntitlementApiException {
-        final List<EntitlementContext> updatedPluginContexts = new LinkedList<EntitlementContext>();
+        final List<DefaultEntitlementContext> updatedPluginContexts = new LinkedList<DefaultEntitlementContext>();
 
         try {
             for (final EntitlementContext pluginContext : pluginContexts) {
@@ -67,7 +67,7 @@ public class EntitlementPluginExecution {
 
             try {
                 for (int i = 0; i < updatedPluginContexts.size(); i++) {
-                    final EntitlementContext updatedPluginContext = updatedPluginContexts.get(i);
+                    final DefaultEntitlementContext updatedPluginContext = updatedPluginContexts.get(i);
                     final WithEntitlementPlugin callback = callbacks.get(i);
 
                     callback.doCall(entitlementApi, updatedPluginContext);
@@ -92,7 +92,7 @@ public class EntitlementPluginExecution {
             if (priorEntitlementResult != null && priorEntitlementResult.isAborted()) {
                 throw new EntitlementApiException(ErrorCode.ENT_PLUGIN_API_ABORTED);
             }
-            final EntitlementContext updatedPluginContext = new DefaultEntitlementContext(pluginContext, priorEntitlementResult);
+            final DefaultEntitlementContext updatedPluginContext = new DefaultEntitlementContext(pluginContext, priorEntitlementResult);
             try {
                 final T result = callback.doCall(entitlementApi, updatedPluginContext);
                 executePluginOnSuccessCalls(updatedPluginContext);
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
index 61563f2..243f576 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2019 Groupon, Inc
+ * Copyright 2014-2019 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
@@ -162,7 +162,7 @@ public class DefaultEntitlementApiBase {
 
         final WithEntitlementPlugin<Void> pauseWithPlugin = new WithEntitlementPlugin<Void>() {
             @Override
-            public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Void doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
                 try {
                     final SubscriptionBase baseSubscription = subscriptionInternalApi.getBaseSubscription(bundleId, internalCallContext);
                     blockUnblockBundle(bundleId, DefaultEntitlementApi.ENT_STATE_BLOCKED, KILLBILL_SERVICES.ENTITLEMENT_SERVICE.getServiceName(), localEffectiveDate, true, true, true, baseSubscription, internalCallContext);
@@ -195,7 +195,7 @@ public class DefaultEntitlementApiBase {
                                                                                internalCallContextFactory.createCallContext(internalCallContext));
         final WithEntitlementPlugin<Void> resumeWithPlugin = new WithEntitlementPlugin<Void>() {
             @Override
-            public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+            public Void doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
                 try {
                     final SubscriptionBase baseSubscription = subscriptionInternalApi.getBaseSubscription(bundleId, internalCallContext);
                     blockUnblockBundle(bundleId, DefaultEntitlementApi.ENT_STATE_CLEAR, KILLBILL_SERVICES.ENTITLEMENT_SERVICE.getServiceName(), localEffectiveDate, false, false, false, baseSubscription, internalCallContext);
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
index d5c9e39..7b00232 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementInternalApi.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2018 Groupon, Inc
- * Copyright 2014-2018 The Billing Project, LLC
+ * Copyright 2014-2019 Groupon, Inc
+ * Copyright 2014-2019 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
@@ -219,7 +219,7 @@ public class DefaultEntitlementInternalApi extends DefaultEntitlementApiBase imp
         }
 
         @Override
-        public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+        public Entitlement doCall(final EntitlementApi entitlementApi, final DefaultEntitlementContext updatedPluginContext) throws EntitlementApiException {
             DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers().iterator().next().getEntitlementEffectiveDate(), updatedPluginContext.getCreatedDate(), internalCallContext);
 
             //
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/logging/EntitlementLoggingHelper.java b/entitlement/src/main/java/org/killbill/billing/entitlement/logging/EntitlementLoggingHelper.java
index e3360d9..a844c84 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/logging/EntitlementLoggingHelper.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/logging/EntitlementLoggingHelper.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2019 Groupon, Inc
+ * Copyright 2014-2019 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
@@ -33,41 +33,6 @@ import org.slf4j.Logger;
 
 public abstract class EntitlementLoggingHelper {
 
-    public static void logCreateEntitlement(final Logger log,
-                                            final UUID bundleId,
-                                            final PlanPhaseSpecifier spec,
-                                            final List<PlanPhasePriceOverride> overrides,
-                                            final LocalDate entitlementDate,
-                                            final LocalDate billingDate) {
-
-        if (log.isInfoEnabled()) {
-            final StringBuilder logLine = new StringBuilder("Create")
-                    .append(bundleId != null ? " AO " : " BP ")
-                    .append("Entitlement: ");
-
-            if (bundleId != null) {
-                logLine.append("bundleId='")
-                       .append(bundleId)
-                       .append("'");
-            }
-            logPlanPhaseSpecifier(logLine, spec, true, true);
-            if (overrides != null && !overrides.isEmpty()) {
-                logPlanPhasePriceOverrides(logLine, overrides);
-            }
-            if (entitlementDate != null) {
-                logLine.append(", entDate='")
-                       .append(entitlementDate)
-                       .append("'");
-            }
-            if (billingDate != null) {
-                logLine.append(", billDate='")
-                       .append(billingDate)
-                       .append("'");
-            }
-            log.info(logLine.toString());
-        }
-    }
-
     public static void logCreateEntitlementsWithAOs(final Logger log, final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementSpecifiersWithAddOns) {
         if (log.isInfoEnabled()) {
             final StringBuilder logLine = new StringBuilder("Create Entitlements with AddOns: ");
@@ -96,16 +61,27 @@ public abstract class EntitlementLoggingHelper {
                    .append("'");
         }
         if (entitlementDate != null) {
-            logLine.append(", entDate='")
+            if (externalKey != null) {
+                logLine.append(", ");
+            }
+            logLine.append("entDate='")
                    .append(entitlementDate)
                    .append("'");
         }
         if (billingDate != null) {
-            logLine.append(", billDate='")
+            if (externalKey != null || entitlementDate != null) {
+                logLine.append(", ");
+            }
+            logLine.append("billDate='")
                    .append(billingDate)
                    .append("'");
         }
-        logEntitlementSpecifier(logLine, entitlementSpecifiers);
+        if (entitlementSpecifiers != null && entitlementSpecifiers.iterator().hasNext()) {
+            if (externalKey != null || entitlementDate != null || billingDate != null) {
+                logLine.append(", ");
+            }
+            logEntitlementSpecifier(logLine, entitlementSpecifiers);
+        }
     }
 
     public static void logPauseResumeEntitlement(final Logger log,
@@ -327,38 +303,44 @@ public abstract class EntitlementLoggingHelper {
     }
 
     private static void logEntitlementSpecifier(final StringBuilder logLine, final Iterable<EntitlementSpecifier> entitlementSpecifiers) {
-        if (entitlementSpecifiers != null && entitlementSpecifiers.iterator().hasNext()) {
-            logLine.append(",'[");
-            boolean first = true;
-            for (EntitlementSpecifier cur : entitlementSpecifiers) {
-                if (!first) {
-                    logLine.append(",");
-                }
-                logPlanPhaseSpecifier(logLine, cur.getPlanPhaseSpecifier(), false, false);
-                logPlanPhasePriceOverrides(logLine, cur.getOverrides());
-                first = false;
+        logLine.append("'[");
+        boolean first = true;
+        for (final EntitlementSpecifier cur : entitlementSpecifiers) {
+            if (!first) {
+                logLine.append(",");
             }
-            logLine.append("]'");
+            logPlanPhaseSpecifier(logLine, cur.getPlanPhaseSpecifier(), false, false);
+            logPlanPhasePriceOverrides(logLine, cur.getOverrides());
+            first = false;
         }
+        logLine.append("]'");
     }
 
-    private static void logPlanPhaseSpecifier(final StringBuilder logLine, final PlanPhaseSpecifier spec, boolean addComma, boolean addParentheseQuote) {
+    private static void logPlanPhaseSpecifier(final StringBuilder logLine, final PlanPhaseSpecifier spec, final boolean addComma, final boolean addParenthesisQuote) {
         if (spec != null) {
             if (addComma) {
                 logLine.append(", ");
             }
-            logLine.append("spec=");
-            if (addParentheseQuote) {
-                logLine.append("'(");
+            if (spec.getPlanName() != null) {
+                logLine.append("planName=");
+                if (addParenthesisQuote) {
+                    logLine.append("'(");
+                }
+                logLine.append(spec.getPlanName());
+            } else {
+                logLine.append("spec=");
+                if (addParenthesisQuote) {
+                    logLine.append("'(");
+                }
+                logLine.append(spec.getProductName() != null ? spec.getProductName() : "null");
+                logLine.append(":");
+                logLine.append(spec.getBillingPeriod() != null ? spec.getBillingPeriod() : "null");
+                logLine.append(":");
+                logLine.append(spec.getPriceListName() != null ? spec.getPriceListName() : "null");
             }
-            logLine.append(spec.getProductName() != null ? spec.getProductName() : "null");
-            logLine.append(":");
-            logLine.append(spec.getBillingPeriod() != null ? spec.getBillingPeriod() : "null");
             logLine.append(":");
             logLine.append(spec.getPhaseType() != null ? spec.getPhaseType() : "null");
-            logLine.append(":");
-            logLine.append(spec.getPriceListName() != null ? spec.getPriceListName() : "null");
-            if (addParentheseQuote) {
+            if (addParenthesisQuote) {
                 logLine.append(")'");
             }
         }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
index fa8da71..a46c4d0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2019 Groupon, Inc
+ * Copyright 2014-2019 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
@@ -19,7 +19,6 @@
 package org.killbill.billing.invoice.generator;
 
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
@@ -34,7 +33,6 @@ import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceApiException;
-import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceStatus;
 import org.killbill.billing.invoice.generator.InvoiceItemGenerator.InvoiceGeneratorResult;
 import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
@@ -42,6 +40,8 @@ import org.killbill.billing.invoice.model.DefaultInvoice;
 import org.killbill.billing.junction.BillingEventSet;
 import org.killbill.billing.util.config.definition.InvoiceConfig;
 import org.killbill.clock.Clock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
@@ -52,6 +52,8 @@ import com.google.inject.Inject;
 
 public class DefaultInvoiceGenerator implements InvoiceGenerator {
 
+    private static final Logger logger = LoggerFactory.getLogger(DefaultInvoiceGenerator.class);
+
     private final Clock clock;
     private final InvoiceConfig config;
 
@@ -134,6 +136,11 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
                 maxDate = invoice.getTargetDate();
             }
         }
+
+        if (targetDate.compareTo(maxDate) != 0) {
+            logger.warn("Adjusting target date from {} to {}", targetDate, maxDate);
+        }
+
         return maxDate;
     }
 
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 cc874e1..cf4deb0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -526,12 +526,12 @@ public class InvoiceDispatcher {
     }
 
     private InvoiceWithFutureNotifications processAccountWithLockAndInputTargetDate(final UUID accountId,
-                                                             final LocalDate targetDate,
-                                                             final BillingEventSet billingEvents,
-                                                             final List<Invoice> existingInvoices,
-                                                             final boolean isDryRun,
-                                                             final boolean isRescheduled,
-                                                             final InternalCallContext internalCallContext) throws InvoiceApiException {
+                                                                                    final LocalDate originalTargetDate,
+                                                                                    final BillingEventSet billingEvents,
+                                                                                    final List<Invoice> existingInvoices,
+                                                                                    final boolean isDryRun,
+                                                                                    final boolean isRescheduled,
+                                                                                    final InternalCallContext internalCallContext) throws InvoiceApiException {
         final CallContext callContext = buildCallContext(internalCallContext);
 
         final ImmutableAccountData account;
@@ -539,11 +539,11 @@ public class InvoiceDispatcher {
             account = accountApi.getImmutableAccountDataById(accountId, internalCallContext);
         } catch (final AccountApiException e) {
             log.error("Unable to generate invoice for accountId='{}', a future notification has NOT been recorded", accountId, e);
-            invoicePluginDispatcher.onFailureCall(targetDate, null, existingInvoices, isDryRun, isRescheduled, callContext, ImmutableList.<PluginProperty>of(), internalCallContext);
+            invoicePluginDispatcher.onFailureCall(originalTargetDate, null, existingInvoices, isDryRun, isRescheduled, callContext, ImmutableList.<PluginProperty>of(), internalCallContext);
             return null;
         }
 
-        final DateTime rescheduleDate = invoicePluginDispatcher.priorCall(targetDate, existingInvoices, isDryRun, isRescheduled, callContext, ImmutableList.<PluginProperty>of(), internalCallContext);
+        final DateTime rescheduleDate = invoicePluginDispatcher.priorCall(originalTargetDate, existingInvoices, isDryRun, isRescheduled, callContext, ImmutableList.<PluginProperty>of(), internalCallContext);
         if (rescheduleDate != null) {
             if (isDryRun) {
                 log.warn("Ignoring rescheduleDate='{}', delayed scheduling is unsupported in dry-run", rescheduleDate);
@@ -554,7 +554,7 @@ public class InvoiceDispatcher {
             return null;
         }
 
-        final InvoiceWithMetadata invoiceWithMetadata = generateKillBillInvoice(account, targetDate, billingEvents, existingInvoices, internalCallContext);
+        final InvoiceWithMetadata invoiceWithMetadata = generateKillBillInvoice(account, originalTargetDate, billingEvents, existingInvoices, internalCallContext);
         final DefaultInvoice invoice = invoiceWithMetadata.getInvoice();
 
         // Compute future notifications
@@ -562,12 +562,12 @@ public class InvoiceDispatcher {
 
         // If invoice comes back null, there is nothing new to generate, we can bail early
         if (invoice == null) {
-            invoicePluginDispatcher.onSuccessCall(targetDate, null, existingInvoices, isDryRun, isRescheduled, callContext, ImmutableList.<PluginProperty>of(), internalCallContext);
+            invoicePluginDispatcher.onSuccessCall(originalTargetDate, null, existingInvoices, isDryRun, isRescheduled, callContext, ImmutableList.<PluginProperty>of(), internalCallContext);
 
             if (isDryRun) {
-                log.info("Generated null dryRun invoice for accountId='{}', targetDate='{}'", accountId, targetDate);
+                log.info("Generated null dryRun invoice for accountId='{}', targetDate='{}'", accountId, originalTargetDate);
             } else {
-                log.info("Generated null invoice for accountId='{}', targetDate='{}'", accountId, targetDate);
+                log.info("Generated null invoice for accountId='{}', targetDate='{}'", accountId, originalTargetDate);
 
                 final BusInternalEvent event = new DefaultNullInvoiceEvent(accountId, clock.getUTCToday(),
                                                                            internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), internalCallContext.getUserToken());
@@ -578,6 +578,7 @@ public class InvoiceDispatcher {
             return null;
         }
 
+        final LocalDate actualTargetDate = invoice.getTargetDate();
         boolean success = false;
         try {
             // Generate missing credit (> 0 for generation and < 0 for use) prior we call the plugin(s)
@@ -609,7 +610,7 @@ public class InvoiceDispatcher {
                 final boolean isRealInvoiceWithItems = uniqueInvoiceIds.remove(invoice.getId());
                 final Set<UUID> adjustedUniqueOtherInvoiceId = uniqueInvoiceIds;
 
-                logInvoiceWithItems(account, invoice, targetDate, adjustedUniqueOtherInvoiceId, isRealInvoiceWithItems);
+                logInvoiceWithItems(account, invoice, actualTargetDate, adjustedUniqueOtherInvoiceId, isRealInvoiceWithItems);
 
                 // Transformation to Invoice -> InvoiceModelDao
                 final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(invoice);
@@ -642,9 +643,9 @@ public class InvoiceDispatcher {
 
             if (isDryRun || success) {
                 final DefaultInvoice refreshedInvoice = isDryRun ? invoice : new DefaultInvoice(invoiceDao.getById(invoice.getId(), internalCallContext));
-                invoicePluginDispatcher.onSuccessCall(targetDate, refreshedInvoice, existingInvoices, isDryRun, isRescheduled, callContext,  ImmutableList.<PluginProperty>of(),internalCallContext);
+                invoicePluginDispatcher.onSuccessCall(actualTargetDate, refreshedInvoice, existingInvoices, isDryRun, isRescheduled, callContext,  ImmutableList.<PluginProperty>of(),internalCallContext);
             } else {
-                invoicePluginDispatcher.onFailureCall(targetDate, invoice, existingInvoices, isDryRun, isRescheduled, callContext, ImmutableList.<PluginProperty>of(), internalCallContext);
+                invoicePluginDispatcher.onFailureCall(actualTargetDate, invoice, existingInvoices, isDryRun, isRescheduled, callContext, ImmutableList.<PluginProperty>of(), internalCallContext);
             }
         }