killbill-aplcache

entitlement: populate bundleId/bundleExternalKey in plugin

2/26/2019 6:59:12 AM

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..0818c27 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;
@@ -62,20 +65,10 @@ public class DefaultEntitlementContext implements EntitlementContext {
              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> 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;
+        }
+    }
+
     @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);
 
             //