killbill-memoizeit

Changes

pom.xml 2(+1 -1)

Details

diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
index b018613..29b10da 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBase.java
@@ -58,6 +58,9 @@ public interface SubscriptionBase extends Entity, Blockable {
     public DateTime changePlan(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context)
             throws SubscriptionBaseApiException;
 
+    public boolean undoChangePlan(final CallContext context)
+            throws SubscriptionBaseApiException;
+
     // Return the effective date of the change
     public DateTime changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate, final CallContext context)
             throws SubscriptionBaseApiException;
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
index 94606d0..2dcf5bd 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseTransitionType.java
@@ -41,6 +41,10 @@ public enum SubscriptionBaseTransitionType {
      */
     UNCANCEL,
     /**
+     * Occurs when a user undo a pending change  before it reached its effective date
+     */
+    UNDO_CHANGE,
+    /**
      * Generated by the system to mark a change of phase
      */
     PHASE,
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 345da81..5491cae 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
@@ -52,6 +52,7 @@ import org.killbill.billing.entitlement.engine.core.EntitlementNotificationKey;
 import org.killbill.billing.entitlement.engine.core.EntitlementNotificationKeyAction;
 import org.killbill.billing.entitlement.engine.core.EntitlementUtils;
 import org.killbill.billing.entitlement.engine.core.EventsStreamBuilder;
+import org.killbill.billing.entitlement.logging.EntitlementLoggingHelper;
 import org.killbill.billing.entitlement.plugin.api.EntitlementContext;
 import org.killbill.billing.entitlement.plugin.api.OperationType;
 import org.killbill.billing.entity.EntityBase;
@@ -80,6 +81,7 @@ import com.google.common.collect.ImmutableList;
 import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logCancelEntitlement;
 import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logChangePlan;
 import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logUncancelEntitlement;
+import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logUndoChangePlan;
 import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logUpdateBCD;
 
 public class DefaultEntitlement extends EntityBase implements Entitlement {
@@ -393,7 +395,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
                 false);
         final List<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifierList = new ArrayList<BaseEntitlementWithAddOnsSpecifier>();
         baseEntitlementWithAddOnsSpecifierList.add(baseEntitlementWithAddOnsSpecifier);
-        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.UNCANCEL_SUBSCRIPTION,
+        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.UNDO_PENDING_SUBSCRIPTION_OPERATION,
                                                                                getAccountId(),
                                                                                null,
                                                                                baseEntitlementWithAddOnsSpecifierList,
@@ -612,6 +614,51 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
     }
 
     @Override
+    public void undoChangePlan(final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
+
+        logUndoChangePlan(log, this);
+
+        checkForPermissions(Permission.ENTITLEMENT_CAN_CHANGE_PLAN, callContext);
+
+        // Get the latest state from disk
+        refresh(callContext);
+
+        final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = new DefaultBaseEntitlementWithAddOnsSpecifier(
+                getBundleId(),
+                getExternalKey(),
+                null,
+                null,
+                null,
+                false);
+        final List<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifierList = new ArrayList<BaseEntitlementWithAddOnsSpecifier>();
+        baseEntitlementWithAddOnsSpecifierList.add(baseEntitlementWithAddOnsSpecifier);
+        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.UNDO_PENDING_SUBSCRIPTION_OPERATION,
+                                                                               getAccountId(),
+                                                                               null,
+                                                                               baseEntitlementWithAddOnsSpecifierList,
+                                                                               null,
+                                                                               properties,
+                                                                               callContext);
+
+        final WithEntitlementPlugin<Void> undoChangePlanEntitlementWithPlugin = new WithEntitlementPlugin<Void>() {
+
+            @Override
+            public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+
+                try {
+                    getSubscriptionBase().undoChangePlan(callContext);
+                } catch (final SubscriptionBaseApiException e) {
+                    throw new EntitlementApiException(e);
+                }
+                return null;
+            }
+        };
+
+        pluginExecution.executeWithPlugin(undoChangePlanEntitlementWithPlugin, pluginContext);
+
+    }
+
+    @Override
     public Entitlement changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, @Nullable final LocalDate effectiveDate, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
 
         logChangePlan(log, this, spec, overrides, effectiveDate, null);
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 0793d6f..a0b158c 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
@@ -214,6 +214,16 @@ public abstract class EntitlementLoggingHelper {
         }
     }
 
+    public static void logUndoChangePlan(final Logger log, final Entitlement entitlement) {
+        if (log.isInfoEnabled()) {
+            final StringBuilder logLine = new StringBuilder("Undo Entitlement Change Plan: ")
+                    .append(" id = '")
+                    .append(entitlement.getId())
+                    .append("'");
+            log.info(logLine.toString());
+        }
+    }
+
     public static void logChangePlan(final Logger log, final Entitlement entitlement, final PlanSpecifier spec,
                                      final List<PlanPhasePriceOverride> overrides, final LocalDate entitlementEffectiveDate, final BillingActionPolicy actionPolicy) {
         if (log.isInfoEnabled()) {
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
index e7e6c9b..7923488 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
@@ -260,6 +260,10 @@ public interface JaxrsResource {
 
     public static final String CBA_REBALANCING = "cbaRebalancing";
 
+
+    public static final String UNDO_CHANGE_PLAN = "undoChangePlan";
+    public static final String UNDO_CANCEL = "uncancel";
+
     public static final String PAUSE = "pause";
     public static final String RESUME = "resume";
     public static final String BLOCK = "block";
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index d54e936..7a9430c 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -499,7 +499,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
 
     @TimedResource
     @PUT
-    @Path("/{subscriptionId:" + UUID_PATTERN + "}/uncancel")
+    @Path("/{subscriptionId:" + UUID_PATTERN + "}/" + UNDO_CANCEL)
     @Produces(APPLICATION_JSON)
     @ApiOperation(value = "Un-cancel an entitlement")
     @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid subscription id supplied"),
@@ -519,6 +519,26 @@ public class SubscriptionResource extends JaxRsResourceBase {
 
     @TimedResource
     @PUT
+    @Path("/{subscriptionId:" + UUID_PATTERN + "}/" + UNDO_CHANGE_PLAN)
+    @Produces(APPLICATION_JSON)
+    @ApiOperation(value = "Undo a pending change plan on an entitlement")
+    @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid subscription id supplied"),
+                           @ApiResponse(code = 404, message = "Entitlement not found")})
+    public Response undoChangeEntitlementPlan(@PathParam("subscriptionId") final String subscriptionId,
+                                            @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
+                                            @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                            @HeaderParam(HDR_REASON) final String reason,
+                                            @HeaderParam(HDR_COMMENT) final String comment,
+                                            @javax.ws.rs.core.Context final HttpServletRequest request) throws EntitlementApiException {
+        final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
+        final UUID uuid = UUID.fromString(subscriptionId);
+        final Entitlement current = entitlementApi.getEntitlementForId(uuid, context.createCallContextNoAccountId(createdBy, reason, comment, request));
+        current.undoChangePlan(pluginProperties, context.createCallContextNoAccountId(createdBy, reason, comment, request));
+        return Response.status(Status.OK).build();
+    }
+
+    @TimedResource
+    @PUT
     @Produces(APPLICATION_JSON)
     @Consumes(APPLICATION_JSON)
     @Path("/{subscriptionId:" + UUID_PATTERN + "}")

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index c5359c3..bb4dedc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.141.5</version>
+        <version>0.141.6-SNAPSHOT</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.19.0-SNAPSHOT</version>
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
index 3eb79f2..94150fa 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
@@ -669,8 +669,61 @@ public class TestEntitlement extends TestJaxrsBase {
         Assert.assertEquals(newEntitlementJson.getBillingPeriod(), BillingPeriod.MONTHLY);
         Assert.assertEquals(newEntitlementJson.getPriceList(), DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
         Assert.assertEquals(newEntitlementJson.getPlanName(), "pistol-monthly");
+    }
+
+    @Test(groups = "slow", description = "Can changePlan and undo changePlan on a subscription")
+    public void testEntitlementUndoChangePlan() throws Exception {
+        final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+        clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+        final Account accountJson = createAccountWithDefaultPaymentMethod();
+
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+
+        final Subscription entitlementJson = createEntitlement(accountJson.getAccountId(), "99999", productName,
+                                                               ProductCategory.BASE, term, true);
+
+
+
+        // Change plan in the future
+        final String newProductName = "Assault-Rifle";
+
+        final Subscription newInput = new Subscription();
+        newInput.setAccountId(entitlementJson.getAccountId());
+        newInput.setSubscriptionId(entitlementJson.getSubscriptionId());
+        newInput.setProductName(newProductName);
+        newInput.setProductCategory(ProductCategory.BASE);
+        newInput.setBillingPeriod(entitlementJson.getBillingPeriod());
+        newInput.setPriceList(entitlementJson.getPriceList());
+
+        Subscription  refreshedSubscription = killBillClient.updateSubscription(newInput, new LocalDate(2012, 4, 28),  null, CALL_COMPLETION_TIMEOUT_SEC, requestOptions);
+        Assert.assertNotNull(refreshedSubscription);
+
+
+        final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(1));
+        clock.addDeltaFromReality(it.toDurationMillis());
+
+        killBillClient.undoChangePlan(refreshedSubscription.getSubscriptionId(), requestOptions);
+
+        // MOVE AFTER TRIAL
+        final Interval it2 = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(30));
+        clock.addDeltaFromReality(it2.toDurationMillis());
+
+        crappyWaitForLackOfProperSynchonization();
+
+        // Retrieves to check EndDate
+        refreshedSubscription = killBillClient.getSubscription(entitlementJson.getSubscriptionId(), requestOptions);
+        Assert.assertEquals(refreshedSubscription.getPriceOverrides().size(), 2);
+        Assert.assertEquals(refreshedSubscription.getPriceOverrides().get(0).getPhaseName(), "shotgun-monthly-trial");
+        Assert.assertEquals(refreshedSubscription.getPriceOverrides().get(0).getFixedPrice(), BigDecimal.ZERO);
+        Assert.assertNull(refreshedSubscription.getPriceOverrides().get(0).getRecurringPrice());
+        Assert.assertEquals(refreshedSubscription.getPriceOverrides().get(1).getPhaseName(), "shotgun-monthly-evergreen");
+        Assert.assertNull(refreshedSubscription.getPriceOverrides().get(1).getFixedPrice());
+        Assert.assertEquals(refreshedSubscription.getPriceOverrides().get(1).getRecurringPrice(), new BigDecimal("249.95"));
 
     }
 
 
+
 }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
index 7bcba98..2740f09 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
@@ -117,4 +117,6 @@ public interface SubscriptionBaseApiService {
                                                              final boolean addCancellationAddOnForEventsIfRequired,
                                                              final Catalog fullCatalog,
                                                              final InternalTenantContext internalTenantContext) throws CatalogApiException;
+
+    boolean undoChangePlan(DefaultSubscriptionBase defaultSubscriptionBase, CallContext context) throws SubscriptionBaseApiException;
 }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
index 1543d9b..30d18ba 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBase.java
@@ -41,7 +41,6 @@ import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanPhase;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
-import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceList;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.catalog.api.ProductCategory;
@@ -280,6 +279,11 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
     }
 
     @Override
+    public boolean undoChangePlan(final CallContext context) throws SubscriptionBaseApiException {
+        return apiService.undoChangePlan(this, context);
+    }
+
+    @Override
     public DateTime changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides,
                                        final DateTime requestedDate, final CallContext context) throws SubscriptionBaseApiException {
         return apiService.changePlanWithRequestedDate(this, spec, overrides, requestedDate, context);
@@ -501,10 +505,10 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
                 prev = curData;
             }
         }
-        // Since UNCANCEL are not part of the transitions, we compute a new 'UNCANCEL' transition based on the event right before that UNCANCEL
-        // This is used to be able to send a bus event for uncancellation
-        if (prev != null && event.getType() == EventType.API_USER && ((ApiEvent) event).getApiEventType() == ApiEventType.UNCANCEL) {
-            final SubscriptionBaseTransitionData withSeq = new SubscriptionBaseTransitionData((SubscriptionBaseTransitionData) prev, EventType.API_USER, ApiEventType.UNCANCEL, seqId);
+        // Since UNCANCEL/UNDO_CHANGE are not part of the transitions, we compute a new transition based on the event right before
+        // This is used to be able to send a bus event for uncancellation/undo_change
+        if (prev != null && event.getType() == EventType.API_USER && (((ApiEvent) event).getApiEventType() == ApiEventType.UNCANCEL || ((ApiEvent) event).getApiEventType() == ApiEventType.UNDO_CHANGE)) {
+            final SubscriptionBaseTransitionData withSeq = new SubscriptionBaseTransitionData(prev, EventType.API_USER, ((ApiEvent) event).getApiEventType(), seqId);
             return withSeq;
         }
         return null;
@@ -569,10 +573,18 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
         throw new SubscriptionBaseError(String.format("Failed to find InitialTransitionForCurrentPlan id = %s", getId()));
     }
 
-    public boolean isSubscriptionFutureCancelled() {
+    public boolean isFutureCancelled() {
         return getFutureEndDate() != null;
     }
 
+
+    public boolean isPendingChangePlan() {
+        final SubscriptionBaseTransition pendingTransition = getPendingTransition();
+        return pendingTransition != null && pendingTransition.getTransitionType() == SubscriptionBaseTransitionType.CHANGE;
+    }
+
+
+
     public DateTime getPlanChangeEffectiveDate(final BillingActionPolicy policy, @Nullable final BillingAlignment alignment, @Nullable final Integer accountBillCycleDayLocal, final InternalTenantContext context) {
 
         final DateTime candidateResult;
@@ -733,6 +745,7 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
                             nextPhaseName = null;
                             break;
                         case UNCANCEL:
+                        case UNDO_CHANGE:
                         default:
                             throw new SubscriptionBaseError(String.format(
                                     "Unexpected UserEvent type = %s", userEV
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
index 60d5928..8f439a9 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -68,6 +68,7 @@ import org.killbill.billing.subscription.events.user.ApiEventChange;
 import org.killbill.billing.subscription.events.user.ApiEventCreate;
 import org.killbill.billing.subscription.events.user.ApiEventType;
 import org.killbill.billing.subscription.events.user.ApiEventUncancel;
+import org.killbill.billing.subscription.events.user.ApiEventUndoChange;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.TenantContext;
@@ -281,7 +282,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
 
     @Override
     public boolean uncancel(final DefaultSubscriptionBase subscription, final CallContext context) throws SubscriptionBaseApiException {
-        if (!subscription.isSubscriptionFutureCancelled()) {
+        if (!subscription.isFutureCancelled()) {
             throw new SubscriptionBaseApiException(ErrorCode.SUB_UNCANCEL_BAD_STATE, subscription.getId().toString());
         }
         try {
@@ -313,7 +314,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
                 uncancelEvents.add(nextPhaseEvent);
             }
 
-            dao.uncancelSubscription(subscription, uncancelEvents, fullCatalog, internalCallContext);
+            dao.uncancelSubscription(subscription, uncancelEvents, internalCallContext);
             subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), fullCatalog);
             return true;
         } catch (final CatalogApiException e) {
@@ -552,6 +553,48 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
     }
 
     @Override
+    public boolean undoChangePlan(final DefaultSubscriptionBase subscription, final CallContext context) throws SubscriptionBaseApiException {
+        if (!subscription.isPendingChangePlan()) {
+            throw new SubscriptionBaseApiException(ErrorCode.SUB_UNDO_CHANGE_BAD_STATE, subscription.getId().toString());
+        }
+        try {
+
+            final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
+            final Catalog fullCatalog = catalogInternalApi.getFullCatalog(true, true, internalCallContext);
+
+            final DateTime now = clock.getUTCNow();
+            final SubscriptionBaseEvent undoChangePlanEvent = new ApiEventUndoChange(new ApiEventBuilder()
+                                                                                     .setSubscriptionId(subscription.getId())
+                                                                                     .setEffectiveDate(now)
+                                                                                     .setFromDisk(true));
+
+            final List<SubscriptionBaseEvent> undoChangePlanEvents = new ArrayList<SubscriptionBaseEvent>();
+            undoChangePlanEvents.add(undoChangePlanEvent);
+
+            //
+            // Used to compute effective for next phase (which was set unactive during cancellation).
+            // In case of a pending subscription we don't want to pass an effective date prior the CREATE event as we would end up with the wrong
+            // transition in PlanAligner (next transition would be CREATE instead of potential next PHASE)
+            //
+            final DateTime planAlignerEffectiveDate = subscription.getState() == EntitlementState.PENDING ? subscription.getStartDate() : now;
+
+            final TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription, planAlignerEffectiveDate, fullCatalog, internalCallContext);
+            final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+                                              PhaseEventData.createNextPhaseEvent(subscription.getId(), nextTimedPhase.getPhase().getName(), nextTimedPhase.getStartPhase()) :
+                                              null;
+            if (nextPhaseEvent != null) {
+                undoChangePlanEvents.add(nextPhaseEvent);
+            }
+
+            dao.undoChangePlan(subscription, undoChangePlanEvents, internalCallContext);
+            subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), fullCatalog);
+            return true;
+        } catch (final CatalogApiException e) {
+            throw new SubscriptionBaseApiException(e);
+        }
+    }
+
+    @Override
     public int handleBasePlanEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent event, final Catalog catalog, final CallContext context) throws CatalogApiException {
 
         final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
@@ -628,7 +671,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
         if (effectiveDate != null && effectiveDate.compareTo(subscription.getStartDate()) < 0) {
             throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_NON_ACTIVE, subscription.getId(), currentState);
         }
-        if (subscription.isSubscriptionFutureCancelled()) {
+        if (subscription.isFutureCancelled()) {
             throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_FUTURE_CANCELLED, subscription.getId());
         }
     }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
index 3c799ea..322f804 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
@@ -24,10 +24,12 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 
 import javax.annotation.Nullable;
@@ -671,7 +673,17 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
     }
 
     @Override
-    public void uncancelSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> uncancelEvents, final Catalog catalog, final InternalCallContext context) {
+    public void uncancelSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> uncancelEvents, final InternalCallContext context) {
+        undoOperation(subscription, uncancelEvents, ApiEventType.CANCEL, SubscriptionBaseTransitionType.UNCANCEL, context);
+    }
+
+    @Override
+    public void undoChangePlan(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> undoChangePlanEvents, final InternalCallContext context) {
+            undoOperation(subscription, undoChangePlanEvents, ApiEventType.CHANGE, SubscriptionBaseTransitionType.UNDO_CHANGE, context);
+    }
+
+
+    private void undoOperation(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> inputEvents, final ApiEventType targetOperation, final SubscriptionBaseTransitionType transitionType, final InternalCallContext context) {
 
         final InternalCallContext contextWithUpdatedDate = contextWithUpdatedDate(context);
         transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@@ -680,23 +692,24 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
                 final SubscriptionEventSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
 
                 final UUID subscriptionId = subscription.getId();
-                SubscriptionEventModelDao cancelledEvent = null;
+
+                Set<SubscriptionEventModelDao> targetEvents = new HashSet<SubscriptionEventModelDao>();
                 final Date now = clock.getUTCNow().toDate();
                 final List<SubscriptionEventModelDao> eventModels = transactional.getFutureActiveEventForSubscription(subscriptionId.toString(), now, contextWithUpdatedDate);
 
                 for (final SubscriptionEventModelDao cur : eventModels) {
-                    if (cur.getUserType() == ApiEventType.CANCEL) {
-                        if (cancelledEvent != null) {
-                            throw new SubscriptionBaseError(String.format("Found multiple cancelWithRequestedDate active events for subscriptions %s", subscriptionId.toString()));
-                        }
-                        cancelledEvent = cur;
+                    if (cur.getEventType() == EventType.API_USER && cur.getUserType() == targetOperation) {
+                        targetEvents.add(cur);
+                    } else if (cur.getEventType() == EventType.PHASE) {
+                        targetEvents.add(cur);
                     }
                 }
 
-                if (cancelledEvent != null) {
-                    final String cancelledEventId = cancelledEvent.getId().toString();
-                    transactional.unactiveEvent(cancelledEventId, contextWithUpdatedDate);
-                    for (final SubscriptionBaseEvent cur : uncancelEvents) {
+                if (!targetEvents.isEmpty()) {
+                    for (SubscriptionEventModelDao target : targetEvents) {
+                        transactional.unactiveEvent(target.getId().toString(), contextWithUpdatedDate);
+                    }
+                    for (final SubscriptionBaseEvent cur : inputEvents) {
                         transactional.create(new SubscriptionEventModelDao(cur), contextWithUpdatedDate);
                         recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory,
                                                                 cur.getEffectiveDate(),
@@ -705,7 +718,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
                     }
 
                     // Notify the Bus of the latest requested change
-                    notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subscription, uncancelEvents.get(uncancelEvents.size() - 1), SubscriptionBaseTransitionType.UNCANCEL, contextWithUpdatedDate);
+                    notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subscription, inputEvents.get(inputEvents.size() - 1), transitionType, contextWithUpdatedDate);
                 }
 
                 return null;
@@ -783,11 +796,12 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
         });
     }
 
+
     private List<SubscriptionBaseEvent> filterSubscriptionBaseEvents(final Collection<SubscriptionEventModelDao> models) {
         final Collection<SubscriptionEventModelDao> filteredModels = Collections2.filter(models, new Predicate<SubscriptionEventModelDao>() {
             @Override
             public boolean apply(@Nullable final SubscriptionEventModelDao input) {
-                return input.getUserType() != ApiEventType.UNCANCEL;
+                return input.getUserType() != ApiEventType.UNCANCEL && input.getUserType() != ApiEventType.UNDO_CHANGE ;
             }
         });
         return new ArrayList<SubscriptionBaseEvent>(Collections2.transform(filteredModels, new Function<SubscriptionEventModelDao, SubscriptionBaseEvent>() {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
index c31b372..4393e40 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
@@ -92,10 +92,12 @@ public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, S
 
     public void cancelSubscriptions(List<DefaultSubscriptionBase> subscriptions, List<SubscriptionBaseEvent> cancelEvents, final Catalog catalog, InternalCallContext context);
 
-    public void uncancelSubscription(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> uncancelEvents, final Catalog catalog, InternalCallContext context);
+    public void uncancelSubscription(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> uncancelEvents, InternalCallContext context);
 
     public void changePlan(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> changeEvents, List<DefaultSubscriptionBase> subscriptionsToBeCancelled, List<SubscriptionBaseEvent> cancelEvents, final Catalog catalog, InternalCallContext context);
 
+    public void undoChangePlan(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> undoChangePlanEvents, InternalCallContext context);
+
     public void transfer(UUID srcAccountId, UUID destAccountId, BundleTransferData data, List<TransferCancelData> transferCancelData, final Catalog catalog, InternalCallContext fromContext, InternalCallContext toContext);
 
     public void updateBundleExternalKey(UUID bundleId, String externalKey, InternalCallContext context);
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java
index 7e3ce7a..e09d14a 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventBuilder.java
@@ -100,6 +100,8 @@ public class ApiEventBuilder extends EventBaseBuilder<ApiEventBuilder> {
             result = new ApiEventCancel(this);
         } else if (apiEventType == ApiEventType.UNCANCEL) {
             result = new ApiEventUncancel(this);
+        } else if (apiEventType == ApiEventType.UNDO_CHANGE) {
+            result = new ApiEventUndoChange(this);
         } else {
             throw new IllegalStateException("Unknown ApiEventType " + apiEventType);
         }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventType.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventType.java
index 1646243..88e4e7c 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventType.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventType.java
@@ -44,6 +44,12 @@ public enum ApiEventType {
             return SubscriptionBaseTransitionType.CANCEL;
         }
     },
+    UNDO_CHANGE {
+        @Override
+        public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
+            return SubscriptionBaseTransitionType.UNDO_CHANGE;
+        }
+    },
     UNCANCEL {
         @Override
         public SubscriptionBaseTransitionType getSubscriptionTransitionType() {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUndoChange.java b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUndoChange.java
new file mode 100644
index 0000000..add830d
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/events/user/ApiEventUndoChange.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 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
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.subscription.events.user;
+
+public class ApiEventUndoChange extends ApiEventBase {
+
+    public ApiEventUndoChange(final ApiEventBuilder builder) {
+        super(builder.setApiEventType(ApiEventType.UNDO_CHANGE));
+    }
+}
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
index 28b6a80..4503630 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
@@ -115,13 +115,13 @@ public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
         aoSubscription.cancel(callContext);
         aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
         assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
-        assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+        assertTrue(aoSubscription.isFutureCancelled());
 
         // CANCEL BASE NOW
         baseSubscription.cancel(callContext);
         baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
         assertEquals(baseSubscription.getState(), EntitlementState.ACTIVE);
-        assertTrue(baseSubscription.isSubscriptionFutureCancelled());
+        assertTrue(baseSubscription.isFutureCancelled());
 
         aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
         List<SubscriptionBaseTransition> aoTransitions = aoSubscription.getAllTransitions();
@@ -183,7 +183,7 @@ public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
         // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
         aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
         assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
-        assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+        assertTrue(aoSubscription.isFutureCancelled());
 
         // MOVE AFTER CANCELLATION
         testListener.pushExpectedEvent(NextEvent.CANCEL);
@@ -237,7 +237,7 @@ public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
         // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
         aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
         assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
-        assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+        assertTrue(aoSubscription.isFutureCancelled());
 
         testListener.pushExpectedEvent(NextEvent.UNCANCEL);
         baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
@@ -246,7 +246,7 @@ public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
 
         aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
         assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
-        assertFalse(aoSubscription.isSubscriptionFutureCancelled());
+        assertFalse(aoSubscription.isFutureCancelled());
 
         // CANCEL AGAIN
         it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(1));
@@ -256,11 +256,11 @@ public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
         baseSubscription.cancel(callContext);
         baseSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(baseSubscription.getId(), internalCallContext);
         assertEquals(baseSubscription.getState(), EntitlementState.ACTIVE);
-        assertTrue(baseSubscription.isSubscriptionFutureCancelled());
+        assertTrue(baseSubscription.isFutureCancelled());
 
         aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
         assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
-        assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+        assertTrue(aoSubscription.isFutureCancelled());
         assertListenerStatus();
     }
 
@@ -372,7 +372,7 @@ public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
         // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
         aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
         assertEquals(aoSubscription.getState(), EntitlementState.ACTIVE);
-        assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+        assertTrue(aoSubscription.isFutureCancelled());
 
         // MOVE AFTER CHANGE
         testListener.pushExpectedEvent(NextEvent.CHANGE);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java
index d7d20b5..e3539fb 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiChangePlan.java
@@ -592,4 +592,44 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
         assertEquals(refreshedSubscription.getAllTransitions().get(1).getTransitionType(), SubscriptionBaseTransitionType.CHANGE);
         assertEquals(refreshedSubscription.getAllTransitions().get(2).getTransitionType(), SubscriptionBaseTransitionType.PHASE);
     }
+
+    @Test(groups = "slow")
+    public void testUndoChangePlan() throws SubscriptionBaseApiException {
+
+        final DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+        clock.setTime(clock.getUTCNow().plusSeconds(1));
+
+        // Change plan in the future
+        final DateTime targetDate = clock.getUTCNow().plusDays(3);
+        subscription.changePlanWithDate(new PlanPhaseSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, targetDate, callContext);assertListenerStatus();
+
+        DefaultSubscriptionBase refreshedSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+        assertEquals(refreshedSubscription.getAllTransitions().size(), 3);
+        assertEquals(refreshedSubscription.getAllTransitions().get(0).getTransitionType(), SubscriptionBaseTransitionType.CREATE);
+        assertEquals(refreshedSubscription.getAllTransitions().get(1).getTransitionType(), SubscriptionBaseTransitionType.CHANGE);
+        assertEquals(refreshedSubscription.getAllTransitions().get(2).getTransitionType(), SubscriptionBaseTransitionType.PHASE);
+
+        clock.addDays(1);
+
+        testListener.pushExpectedEvent(NextEvent.UNDO_CHANGE);
+        subscription.undoChangePlan(callContext);
+        assertListenerStatus();
+
+        // No CHANGE_PLAN
+        clock.addDays(3);
+        assertListenerStatus();
+
+        // Verify PHASE event for Pistol is actif
+        testListener.pushExpectedEvent(NextEvent.PHASE);
+        clock.addDays(26);
+        assertListenerStatus();
+
+
+        refreshedSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
+        assertEquals(refreshedSubscription.getAllTransitions().size(), 2);
+        assertEquals(refreshedSubscription.getAllTransitions().get(0).getTransitionType(), SubscriptionBaseTransitionType.CREATE);
+        assertEquals(refreshedSubscription.getAllTransitions().get(1).getTransitionType(), SubscriptionBaseTransitionType.PHASE);
+
+    }
 }
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
index ba31884..8bf6081 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -377,6 +377,7 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
         cancelSubscriptions(subscriptionsToBeCancelled, cancelEvents, catalog, context);
     }
 
+
     private void insertEvent(final SubscriptionBaseEvent event, final InternalCallContext context) {
         synchronized (events) {
             events.add(event);
@@ -432,10 +433,20 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
 
     @Override
     public void uncancelSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> uncancelEvents,
-                                     final Catalog catalog, final InternalCallContext context) {
+                                     final InternalCallContext context) {
+        undoPendingOperation(subscription, uncancelEvents, ApiEventType.CANCEL, context);
+    }
+        @Override
+    public void undoChangePlan(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> undoChangePlanEvents, final InternalCallContext context) {
+            undoPendingOperation(subscription, undoChangePlanEvents, ApiEventType.CHANGE, context);
+    }
+
+
+    private void undoPendingOperation(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> inputEvents,
+                                     final ApiEventType  targetType, final InternalCallContext context) {
 
         synchronized (events) {
-            boolean foundCancel = false;
+            boolean foundEvent = false;
             final Iterator<SubscriptionBaseEvent> it = events.descendingIterator();
             while (it.hasNext()) {
                 final SubscriptionBaseEvent cur = it.next();
@@ -443,14 +454,14 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
                     continue;
                 }
                 if (cur.getType() == EventType.API_USER &&
-                    ((ApiEvent) cur).getApiEventType() == ApiEventType.CANCEL) {
+                    ((ApiEvent) cur).getApiEventType() == targetType) {
                     it.remove();
-                    foundCancel = true;
+                    foundEvent = true;
                     break;
                 }
             }
-            if (foundCancel) {
-                for (final SubscriptionBaseEvent cur : uncancelEvents) {
+            if (foundEvent) {
+                for (final SubscriptionBaseEvent cur : inputEvents) {
                     insertEvent(cur, context);
                 }
             }
diff --git a/util/src/test/java/org/killbill/billing/api/TestApiListener.java b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
index fd0bc8c..d730885 100644
--- a/util/src/test/java/org/killbill/billing/api/TestApiListener.java
+++ b/util/src/test/java/org/killbill/billing/api/TestApiListener.java
@@ -106,6 +106,7 @@ public class TestApiListener {
         CREATE,
         TRANSFER,
         CHANGE,
+        UNDO_CHANGE,
         CANCEL,
         UNCANCEL,
         PAUSE,
@@ -164,6 +165,10 @@ public class TestApiListener {
                 assertEqualsNicely(NextEvent.CHANGE);
                 notifyIfStackEmpty();
                 break;
+            case UNDO_CHANGE:
+                assertEqualsNicely(NextEvent.UNDO_CHANGE);
+                notifyIfStackEmpty();
+                break;
             case UNCANCEL:
                 assertEqualsNicely(NextEvent.UNCANCEL);
                 notifyIfStackEmpty();
diff --git a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
index 146a130..7a72c14 100644
--- a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
+++ b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
@@ -89,6 +89,11 @@ public class MockSubscription implements SubscriptionBase {
     }
 
     @Override
+    public boolean undoChangePlan(final CallContext context) throws SubscriptionBaseApiException {
+        return sub.undoChangePlan(context);
+    }
+
+    @Override
     public DateTime changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate,
                                        final CallContext context) throws SubscriptionBaseApiException {
         return sub.changePlanWithDate(spec, overrides, requestedDate, context);