killbill-aplcache

subscription, entitlement, jaxrs: Modify entitlement apis

8/22/2016 5:27:49 PM

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 5382bba..5cda4c4 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
@@ -26,6 +26,7 @@ import org.killbill.billing.catalog.api.BillingPeriod;
 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.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceList;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.catalog.api.ProductCategory;
@@ -52,15 +53,15 @@ public interface SubscriptionBase extends Entity, Blockable {
             throws SubscriptionBaseApiException;
 
     // Return the effective date of the change
-    public DateTime changePlan(final String productName, final BillingPeriod term, final String priceList, final List<PlanPhasePriceOverride> overrides, final CallContext context)
+    public DateTime changePlan(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context)
             throws SubscriptionBaseApiException;
 
     // Return the effective date of the change
-    public DateTime changePlanWithDate(final String productName, final BillingPeriod term, final String priceList, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate, final CallContext context)
+    public DateTime changePlanWithDate(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate, final CallContext context)
             throws SubscriptionBaseApiException;
 
     // Return the effective date of the change
-    public DateTime changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList, final List<PlanPhasePriceOverride> overrides,
+    public DateTime changePlanWithPolicy(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides,
                                          final BillingActionPolicy policy, final CallContext context)
             throws SubscriptionBaseApiException;
 
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index 2556f87..234f9f3 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -34,6 +34,7 @@ import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.CatalogApiException;
 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.entitlement.api.EntitlementAOStatusDryRun;
 import org.killbill.billing.entitlement.api.EntitlementSpecifier;
 import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
@@ -88,8 +89,7 @@ public interface SubscriptionBaseInternalApi {
 
     public List<EffectiveSubscriptionInternalEvent> getBillingTransitions(SubscriptionBase subscription, InternalTenantContext context);
 
-    public DateTime getDryRunChangePlanEffectiveDate(SubscriptionBase subscription, String productName, BillingPeriod term,
-                                                     String priceList, DateTime requestedDate, BillingActionPolicy policy, InternalTenantContext context) throws SubscriptionBaseApiException;
+    public DateTime getDryRunChangePlanEffectiveDate(SubscriptionBase subscription, PlanSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, InternalTenantContext context) throws SubscriptionBaseApiException;
 
     public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(UUID subscriptionId, @Nullable String baseProductName,
                                                                      DateTime requestedDate, InternalTenantContext context) throws SubscriptionBaseApiException;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index 9f64e78..beb7246 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -34,6 +34,7 @@ import org.killbill.billing.beatrix.integration.BeatrixIntegrationModule;
 import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.BlockingApiException;
@@ -995,7 +996,7 @@ public class TestOverdueIntegration extends TestOverdueBase {
     private void checkChangePlanWithOverdueState(final Entitlement entitlement, final boolean shouldFail, final boolean expectedPayment) {
         if (shouldFail) {
             try {
-                entitlement.changePlan("Pistol", term, PriceListSet.DEFAULT_PRICELIST_NAME, null, ImmutableList.<PluginProperty>of(), callContext);
+                entitlement.changePlan(new PlanSpecifier("Pistol", term, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
             } catch (EntitlementApiException e) {
                 assertTrue(e.getCause() instanceof BlockingApiException || e.getCode() == ErrorCode.SUB_CHANGE_NON_ACTIVE.getCode(),
                            String.format("Cause is %s, message is %s", e.getCause(), e.getMessage()));
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
index 919890e..f893c7e 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestCatalogRetireElements.java
@@ -20,6 +20,7 @@ package org.killbill.billing.beatrix.integration;
 import java.util.List;
 
 import org.joda.time.LocalDate;
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingPeriod;
@@ -84,7 +85,7 @@ public class TestCatalogRetireElements extends TestIntegrationBase {
             entitlementApi.createBaseEntitlement(account.getId(), spec, "externalKey2", null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
             fail(); // force to fail is there is not an exception
         } catch (final EntitlementApiException e) {
-            assertTrue(e.getLocalizedMessage().startsWith("Could not find a plan matching: (product: 'Pistol', billing period: 'MONTHLY'"));
+            assertEquals(e.getCode(), ErrorCode.CAT_PLAN_NOT_FOUND.getCode());
         }
 
         // Move out a month and verify 'Pistol' plan continue working as expected.
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index 59a72b9..bab10ab 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -53,6 +53,7 @@ import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.catalog.api.PhaseType;
 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.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
@@ -665,9 +666,9 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
                     // Need to fetch again to get latest CTD updated from the system
                     Entitlement refreshedEntitlement = entitlementApi.getEntitlementForId(entitlement.getId(), callContext);
                     if (billingPolicy == null) {
-                        refreshedEntitlement = refreshedEntitlement.changePlan(productName, billingPeriod, priceList, null, ImmutableList.<PluginProperty>of(), callContext);
+                        refreshedEntitlement = refreshedEntitlement.changePlan(new PlanSpecifier(productName, billingPeriod, priceList), null, ImmutableList.<PluginProperty>of(), callContext);
                     } else {
-                        refreshedEntitlement = refreshedEntitlement.changePlanOverrideBillingPolicy(productName, billingPeriod, priceList, null, null, billingPolicy, ImmutableList.<PluginProperty>of(), callContext);
+                        refreshedEntitlement = refreshedEntitlement.changePlanOverrideBillingPolicy(new PlanSpecifier(productName, billingPeriod, priceList), null, null, billingPolicy, ImmutableList.<PluginProperty>of(), callContext);
                     }
                     return refreshedEntitlement;
                 } catch (final EntitlementApiException e) {
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
index 7724af0..e759270 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
@@ -27,6 +27,7 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
 import org.killbill.billing.invoice.api.Invoice;
@@ -173,7 +174,7 @@ public class TestIntegrationParentInvoice extends TestIntegrationBase {
 
         // upgrade plan
         busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE);
-        baseEntitlementChild.changePlanOverrideBillingPolicy("Shotgun", BillingPeriod.MONTHLY, baseEntitlementChild.getLastActivePriceList().getName(), null, clock.getToday(childAccount.getTimeZone()), BillingActionPolicy.IMMEDIATE, null, callContext);
+        baseEntitlementChild.changePlanOverrideBillingPolicy(new PlanSpecifier("Shotgun", BillingPeriod.MONTHLY, baseEntitlementChild.getLastActivePriceList().getName()), null, clock.getToday(childAccount.getTimeZone()), BillingActionPolicy.IMMEDIATE, null, callContext);
         assertListenerStatus();
 
         // check parent invoice. Expected to have the same invoice item with the amount updated
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
index 0bbd002..c032ac5 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
@@ -18,8 +18,6 @@
 package org.killbill.billing.beatrix.integration;
 
 import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.UUID;
 
 import javax.inject.Inject;
@@ -32,8 +30,10 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
 import org.killbill.billing.callcontext.DefaultCallContext;
 import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.CatalogUserApi;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.catalog.api.SimplePlanDescriptor;
@@ -94,7 +94,7 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
         StaticCatalog catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
         assertEquals(catalog.getCurrentPlans().length, 1);
 
-        final Entitlement baseEntitlement = createEntitlement("Foo", BillingPeriod.MONTHLY);
+        final Entitlement baseEntitlement = createEntitlement("foo-monthly", true);
 
         invoiceChecker.checkInvoice(account.getId(), 1, testCallContext, new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 1), new LocalDate(2016, 7, 1), InvoiceItemType.RECURRING, BigDecimal.TEN));
 
@@ -104,10 +104,9 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
         catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
         assertEquals(catalog.getCurrentPlans().length, 2);
 
-
         // Change Plan to the newly added Plan and verify correct default rules behavior (IMMEDIATE change)
         busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
-        baseEntitlement.changePlan("SuperFoo", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, ImmutableList.<PluginProperty>of(), testCallContext);
+        baseEntitlement.changePlan(new PlanSpecifier("SuperFoo", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), testCallContext);
         assertListenerStatus();
 
         invoiceChecker.checkInvoice(account.getId(), 2, testCallContext,
@@ -116,17 +115,61 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
         assertListenerStatus();
     }
 
+    @Test(groups = "slow")
+    public void testWithMultiplePlansForOneProduct() throws CatalogApiException, EntitlementApiException {
+
+        // Create a per-tenant catalog with one plan
+        final SimplePlanDescriptor desc1 = new DefaultSimplePlanDescriptor("xxx-monthly", "XXX", ProductCategory.BASE, account.getCurrency(), BigDecimal.TEN, BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of());
+        catalogUserApi.addSimplePlan(desc1, init, testCallContext);
+        StaticCatalog catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
+        assertEquals(catalog.getCurrentProducts().length, 1);
+        assertEquals(catalog.getCurrentPlans().length, 1);
+
+        final Entitlement baseEntitlement1 = createEntitlement("xxx-monthly", true);
+
+        // Add a second plan for same product but with a 14 days trial
+        final SimplePlanDescriptor desc2 = new DefaultSimplePlanDescriptor("xxx-14-monthly", "XXX", ProductCategory.BASE, account.getCurrency(), BigDecimal.TEN, BillingPeriod.MONTHLY, 14, TimeUnit.DAYS, ImmutableList.<String>of());
+        catalogUserApi.addSimplePlan(desc2, init, testCallContext);
+        catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
+        assertEquals(catalog.getCurrentProducts().length, 1);
+        assertEquals(catalog.getCurrentPlans().length, 2);
 
-    private Entitlement createEntitlement(String productName, BillingPeriod billingPeriod) throws EntitlementApiException {
-        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(productName, billingPeriod, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final Entitlement baseEntitlement2 = createEntitlement("xxx-14-monthly", false);
 
-        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+
+        // Add a second plan for same product but with a 30 days trial
+        final SimplePlanDescriptor desc3 = new DefaultSimplePlanDescriptor("xxx-30-monthly", "XXX", ProductCategory.BASE, account.getCurrency(), BigDecimal.TEN, BillingPeriod.MONTHLY, 30, TimeUnit.DAYS, ImmutableList.<String>of());
+        catalogUserApi.addSimplePlan(desc3, init, testCallContext);
+        catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
+        assertEquals(catalog.getCurrentProducts().length, 1);
+        assertEquals(catalog.getCurrentPlans().length, 3);
+
+        final Entitlement baseEntitlement3 = createEntitlement("xxx-30-monthly", false);
+
+        // Move clock 14 days
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addDays(14);
+        assertListenerStatus();
+
+        // Move clock 16 days
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.INVOICE, NextEvent.NULL_INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        clock.addDays(16);
+        assertListenerStatus();
+    }
+
+    private Entitlement createEntitlement(final String planName, final boolean expectPayment) throws EntitlementApiException {
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(planName, null);
+
+        if (expectPayment) {
+            busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+        } else {
+            busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        }
         final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, UUID.randomUUID().toString(), null, null, null, false, ImmutableList.<PluginProperty>of(), testCallContext);
         assertListenerStatus();
         return entitlement;
     }
 
-
     private void setupTenant() throws TenantApiException {
         final UUID uuid = UUID.randomUUID();
         final String externalKey = uuid.toString();
@@ -136,7 +179,7 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
         tenant = tenantUserApi.createTenant(new DefaultTenant(uuid, init, init, externalKey, apiKey, apiSecret), callContext);
 
         testCallContext = new DefaultCallContext(tenant.getId(), "tester", CallOrigin.EXTERNAL, UserType.TEST,
-                                      "good reason", "trust me", uuid, clock);
+                                                 "good reason", "trust me", uuid, clock);
     }
 
     private void setupAccount() throws Exception {
@@ -146,7 +189,7 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
         assertNotNull(account);
 
         final PaymentMethodPlugin info = createPaymentMethodPlugin();
-        paymentApi.addPaymentMethod(account, UUID.randomUUID().toString(),  BeatrixIntegrationModule.NON_OSGI_PLUGIN_NAME, true, info, PLUGIN_PROPERTIES, testCallContext);
+        paymentApi.addPaymentMethod(account, UUID.randomUUID().toString(), BeatrixIntegrationModule.NON_OSGI_PLUGIN_NAME, true, info, PLUGIN_PROPERTIES, testCallContext);
     }
 
 }
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithPriceOverride.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithPriceOverride.java
index 230187b..0869e63 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithPriceOverride.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithPriceOverride.java
@@ -32,6 +32,7 @@ import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
@@ -120,7 +121,7 @@ public class TestWithPriceOverride extends TestIntegrationBase {
         overrides.add(new DefaultPlanPhasePriceOverride("shotgun-monthly-evergreen", account.getCurrency(), null, new BigDecimal("279.95")));
 
         busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
-        bpSubscription.changePlanOverrideBillingPolicy("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, overrides, new LocalDate(2012, 5, 1), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+        bpSubscription.changePlanOverrideBillingPolicy(new PlanSpecifier("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), overrides, new LocalDate(2012, 5, 1), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
         assertListenerStatus();
 
         invoiceChecker.checkInvoice(account.getId(), 3, callContext,
@@ -167,7 +168,7 @@ public class TestWithPriceOverride extends TestIntegrationBase {
         overrides.add(new DefaultPlanPhasePriceOverride("telescopic-scope-monthly-evergreen", account.getCurrency(), null, new BigDecimal("1200.00")));
 
         busHandler.pushExpectedEvents(NextEvent.CHANGE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
-        aoEntitlement.changePlanOverrideBillingPolicy("Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, overrides, new LocalDate(2012, 5, 5), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+        aoEntitlement.changePlanOverrideBillingPolicy(new PlanSpecifier("Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), overrides, new LocalDate(2012, 5, 5), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
         assertListenerStatus();
     }
 }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java
index f912ecd..0d3b457 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java
@@ -82,31 +82,9 @@ public class DefaultPriceList extends ValidatingConfig<StandaloneCatalog> implem
 
     @Override
     public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
-        if (getPlans() != null) {
-            for (final DefaultPlan cur : getPlans()) {
-                final int numPlans = findNumberOfPlans(cur.getProduct(), cur.getRecurringBillingPeriod());
-                if (numPlans > 1) {
-                    errors.add(new ValidationError(
-                            String.format("There are %d plans in pricelist %s and have the same product/billingPeriod (%s, %s)",
-                                          numPlans, getName(), cur.getProduct().getName(), cur.getRecurringBillingPeriod()), catalog.getCatalogURI(),
-                            DefaultPriceListSet.class, getName()));
-                }
-            }
-        }
         return errors;
     }
 
-    private int findNumberOfPlans(final Product product, final BillingPeriod period) {
-        int count = 0;
-        for (final DefaultPlan cur : getPlans()) {
-            if (cur.getProduct().equals(product) &&
-                    (cur.getRecurringBillingPeriod() == null || cur.getRecurringBillingPeriod().equals(period))) {
-                count++;
-            }
-        }
-        return count;
-    }
-
     public DefaultPriceList setName(final String name) {
         this.name = name;
         return this;
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java
index f05318d..a7511f4 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java
@@ -46,6 +46,7 @@ import org.killbill.billing.catalog.api.PlanPhasePriceOverridesWithCallContext;
 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.PriceListSet;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.catalog.api.StaticCatalog;
@@ -168,17 +169,26 @@ public class StandaloneCatalog extends ValidatingConfig<StandaloneCatalog> imple
       */
     @Override
     public DefaultPlan createOrFindCurrentPlan(final PlanSpecifier spec, final PlanPhasePriceOverridesWithCallContext unused) throws CatalogApiException {
-        if (spec.getProductName() == null) {
-            throw new CatalogApiException(ErrorCode.CAT_NULL_PRODUCT_NAME);
-        }
-        if (priceLists == null) {
-            throw new CatalogApiException(ErrorCode.CAT_PRICE_LIST_NOT_FOUND, spec.getPriceListName());
+        final DefaultPlan result;
+        if (spec.getPlanName() != null) {
+            result = findCurrentPlan(spec.getPlanName());
+        } else {
+            if (spec.getProductName() == null) {
+                throw new CatalogApiException(ErrorCode.CAT_NULL_PRODUCT_NAME);
+            }
+            if (spec.getBillingPeriod() == null) {
+                throw new CatalogApiException(ErrorCode.CAT_NULL_BILLING_PERIOD);
+            }
+            final String inputOrDefaultPricelist = (spec.getPriceListName() == null) ? PriceListSet.DEFAULT_PRICELIST_NAME : spec.getPriceListName();
+            final Product product = findCurrentProduct(spec.getProductName());
+            result = priceLists.getPlanFrom(inputOrDefaultPricelist, product, spec.getBillingPeriod());
         }
-        final Product product = findCurrentProduct(spec.getProductName());
-        final DefaultPlan result = priceLists.getPlanFrom(spec.getPriceListName(), product, spec.getBillingPeriod());
         if (result == null) {
-            final String periodString = (spec.getBillingPeriod() == null) ? "NULL" : spec.getBillingPeriod().toString();
-            throw new CatalogApiException(ErrorCode.CAT_PLAN_NOT_FOUND, spec.getProductName(), periodString, spec.getPriceListName());
+            throw new CatalogApiException(ErrorCode.CAT_PLAN_NOT_FOUND,
+                                          spec.getPlanName() !=  null ? spec.getPlanName() : "undefined",
+                                          spec.getProductName() != null ? spec.getProductName() : "undefined",
+                                          spec.getBillingPeriod() != null ? spec.getBillingPeriod() : "undefined",
+                                          spec.getPriceListName() != null ? spec.getPriceListName() : "undefined");
         }
         return result;
     }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
index 90e499e..ea97bcb 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
@@ -128,11 +128,7 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
 
 
         public Plan findPlan(final StandaloneCatalog catalog) throws CatalogApiException {
-            if (spec.getPlanName() != null) {
-                return catalog.findCurrentPlan(spec.getPlanName());
-            } else {
-                return catalog.createOrFindCurrentPlan(new PlanSpecifier(spec.getProductName(), spec.getBillingPeriod(), spec.getPriceListName()), overrides);
-            }
+            return catalog.createOrFindCurrentPlan(spec, overrides);
         }
     }
 
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 36f5347..3e5fa05 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
@@ -38,6 +38,7 @@ import org.killbill.billing.catalog.api.BillingPeriod;
 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.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceList;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.catalog.api.ProductCategory;
@@ -526,9 +527,9 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
     }
 
     @Override
-    public Entitlement changePlan(final String productName, final BillingPeriod billingPeriod, final String priceList, final List<PlanPhasePriceOverride> overrides, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
+    public Entitlement changePlan(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
 
-        logChangePlan(log, this, productName, billingPeriod, priceList, overrides, null, null);
+        logChangePlan(log, this, spec, overrides, null, null);
 
         checkForPermissions(Permission.ENTITLEMENT_CAN_CHANGE_PLAN, callContext);
 
@@ -558,7 +559,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
                 final DateTime effectiveChangeDate;
                 try {
-                    effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), productName, billingPeriod, priceList, null, null, context);
+                    effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, null, context);
                 } catch (final SubscriptionBaseApiException e) {
                     throw new EntitlementApiException(e, e.getCode(), e.getMessage());
                 }
@@ -570,7 +571,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
                 }
 
                 try {
-                    getSubscriptionBase().changePlan(productName, billingPeriod, priceList, overrides, callContext);
+                    getSubscriptionBase().changePlan(spec, overrides, callContext);
                 } catch (final SubscriptionBaseApiException e) {
                     throw new EntitlementApiException(e);
                 }
@@ -590,9 +591,9 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
     }
 
     @Override
-    public Entitlement changePlanWithDate(final String productName, final BillingPeriod billingPeriod, final String priceList, final List<PlanPhasePriceOverride> overrides, @Nullable final LocalDate effectiveDate, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
+    public Entitlement changePlanWithDate(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, @Nullable final LocalDate effectiveDate, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
 
-        logChangePlan(log, this, productName, billingPeriod, priceList, overrides, effectiveDate, null);
+        logChangePlan(log, this, spec, overrides, effectiveDate, null);
 
         checkForPermissions(Permission.ENTITLEMENT_CAN_CHANGE_PLAN, callContext);
 
@@ -623,7 +624,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
                 final DateTime resultingEffectiveDate;
                 try {
-                    resultingEffectiveDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), productName, billingPeriod, priceList, effectiveChangeDate, null, context);
+                    resultingEffectiveDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, effectiveChangeDate, null, context);
                 } catch (final SubscriptionBaseApiException e) {
                     throw new EntitlementApiException(e, e.getCode(), e.getMessage());
                 }
@@ -635,7 +636,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
                 }
 
                 try {
-                    getSubscriptionBase().changePlanWithDate(productName, billingPeriod, priceList, overrides, resultingEffectiveDate, callContext);
+                    getSubscriptionBase().changePlanWithDate(spec, overrides, resultingEffectiveDate, callContext);
                 } catch (final SubscriptionBaseApiException e) {
                     throw new EntitlementApiException(e);
                 }
@@ -656,9 +657,9 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
     }
 
     @Override
-    public Entitlement changePlanOverrideBillingPolicy(final String productName, final BillingPeriod billingPeriod, final String priceList, final List<PlanPhasePriceOverride> overrides, final LocalDate entitlementEffectiveDate, final BillingActionPolicy actionPolicy, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
+    public Entitlement changePlanOverrideBillingPolicy(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final LocalDate entitlementEffectiveDate, final BillingActionPolicy actionPolicy, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
 
-        logChangePlan(log, this, productName, billingPeriod, priceList, overrides, entitlementEffectiveDate, actionPolicy);
+        logChangePlan(log, this, spec, overrides, entitlementEffectiveDate, actionPolicy);
 
         checkForPermissions(Permission.ENTITLEMENT_CAN_CHANGE_PLAN, callContext);
 
@@ -688,7 +689,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
                 final DateTime effectiveChangeDate;
                 try {
-                    effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), productName, billingPeriod, priceList, null, actionPolicy, context);
+                    effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, actionPolicy, context);
                 } catch (final SubscriptionBaseApiException e) {
                     throw new EntitlementApiException(e, e.getCode(), e.getMessage());
                 }
@@ -700,7 +701,7 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
                 }
 
                 try {
-                    getSubscriptionBase().changePlanWithPolicy(productName, billingPeriod, priceList, overrides, actionPolicy, callContext);
+                    getSubscriptionBase().changePlanWithPolicy(spec, overrides, actionPolicy, callContext);
                 } catch (final SubscriptionBaseApiException e) {
                     throw new EntitlementApiException(e);
                 }
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 743cc8a..7e7f2e2 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
@@ -25,6 +25,7 @@ import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 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.entitlement.api.BlockingState;
 import org.killbill.billing.entitlement.api.Entitlement;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
@@ -197,7 +198,7 @@ public abstract class EntitlementLoggingHelper {
         }
     }
 
-    public static void logChangePlan(final Logger log, final Entitlement entitlement, String productName, final BillingPeriod billingPeriod, final String priceList,
+    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()) {
             final StringBuilder logLine = new StringBuilder("Change Entitlement Plan: ")
@@ -209,19 +210,24 @@ public abstract class EntitlementLoggingHelper {
                        .append(entitlementEffectiveDate)
                        .append("'");
             }
-            if (productName != null) {
+            if (spec.getPlanName() != null) {
+                logLine.append(", plan='")
+                       .append(spec.getPlanName())
+                       .append("'");
+            }
+            if (spec.getProductName() != null) {
                 logLine.append(", product='")
-                       .append(productName)
+                       .append(spec.getProductName())
                        .append("'");
             }
-            if (billingPeriod != null) {
+            if (spec.getBillingPeriod() != null) {
                 logLine.append(", billingPeriod='")
-                       .append(billingPeriod)
+                       .append(spec.getBillingPeriod())
                        .append("'");
             }
-            if (priceList != null) {
+            if (spec.getPriceListName() != null) {
                 logLine.append(", priceList='")
-                       .append(priceList)
+                       .append(spec.getBillingPeriod())
                        .append("'");
             }
             logPlanPhasePriceOverrides(logLine, overrides);
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
index 15cb7cb..692995f 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
@@ -27,6 +27,7 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.EntitlementTestSuiteWithEmbeddedDB;
@@ -262,7 +263,7 @@ public class TestDefaultEntitlement extends EntitlementTestSuiteWithEmbeddedDB {
 
         // Immediate change during trial
         testListener.pushExpectedEvent(NextEvent.CHANGE);
-        entitlement.changePlan("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, ImmutableList.<PluginProperty>of(), callContext);
+        entitlement.changePlan(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
         assertListenerStatus();
 
         // Verify the change is immediate
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionApi.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionApi.java
index aef34c1..18a4beb 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionApi.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionApi.java
@@ -30,6 +30,7 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.EntitlementService;
@@ -498,7 +499,7 @@ public class TestDefaultSubscriptionApi extends EntitlementTestSuiteWithEmbedded
         assertListenerStatus();
 
         try {
-            entitlement.changePlan("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, ImmutableList.<PluginProperty>of(), callContext);
+            entitlement.changePlan(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
             fail();
         } catch (final EntitlementApiException e) {
             assertEquals(e.getCode(), ErrorCode.BLOCK_BLOCKED_ACTION.getCode());
@@ -507,7 +508,7 @@ public class TestDefaultSubscriptionApi extends EntitlementTestSuiteWithEmbedded
         }
 
         try {
-            entitlement.changePlanWithDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, clock.getUTCToday(), ImmutableList.<PluginProperty>of(), callContext);
+            entitlement.changePlanWithDate(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCToday(), ImmutableList.<PluginProperty>of(), callContext);
             fail();
         } catch (final EntitlementApiException e) {
             assertEquals(e.getCode(), ErrorCode.BLOCK_BLOCKED_ACTION.getCode());
@@ -516,7 +517,7 @@ public class TestDefaultSubscriptionApi extends EntitlementTestSuiteWithEmbedded
         }
 
         try {
-            entitlement.changePlanOverrideBillingPolicy("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
+            entitlement.changePlanOverrideBillingPolicy(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, ImmutableList.<PluginProperty>of(), callContext);
             fail();
         } catch (final EntitlementApiException e) {
             assertEquals(e.getCode(), ErrorCode.BLOCK_BLOCKED_ACTION.getCode());
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/engine/core/TestEntitlementUtils.java b/entitlement/src/test/java/org/killbill/billing/entitlement/engine/core/TestEntitlementUtils.java
index 589189b..301d6f0 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/engine/core/TestEntitlementUtils.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/engine/core/TestEntitlementUtils.java
@@ -31,6 +31,7 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.AccountEventsStreams;
@@ -232,7 +233,7 @@ public class TestEntitlementUtils extends EntitlementTestSuiteWithEmbeddedDB {
     @Test(groups = "slow", description = "Verify add-ons blocking states are added for EOT change plans")
     public void testChangePlanEOT() throws Exception {
         // Change plan EOT to Assault-Rifle (Telescopic-Scope is included)
-        final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlanWithDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, new LocalDate(2013, 10, 7), ImmutableList.<PluginProperty>of(), callContext);
+        final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlanWithDate(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, new LocalDate(2013, 10, 7), ImmutableList.<PluginProperty>of(), callContext);
         // No blocking event (EOT)
         assertListenerStatus();
 
@@ -271,7 +272,7 @@ public class TestEntitlementUtils extends EntitlementTestSuiteWithEmbeddedDB {
         assertListenerStatus();
 
         // Change plan EOT to Assault-Rifle (Telescopic-Scope is included)
-        final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlanWithDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, new LocalDate(2013, 10, 7), ImmutableList.<PluginProperty>of(), callContext);
+        final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlanWithDate(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, new LocalDate(2013, 10, 7), ImmutableList.<PluginProperty>of(), callContext);
         // No blocking event (EOT)
         assertListenerStatus();
 
@@ -290,7 +291,7 @@ public class TestEntitlementUtils extends EntitlementTestSuiteWithEmbeddedDB {
 
         // Change plan IMM (upgrade) to Assault-Rifle (Telescopic-Scope is included)
         testListener.pushExpectedEvents(NextEvent.CHANGE, NextEvent.CANCEL, NextEvent.BLOCK);
-        final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlan("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, ImmutableList.<PluginProperty>of(), callContext);
+        final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlan(new PlanSpecifier("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, ImmutableList.<PluginProperty>of(), callContext);
         assertListenerStatus();
 
         // We need to add a 1s delay before invoking the eventsStreamBuilder in the checks below, because
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
index 2365957..d83acaa 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PhasePriceOverrideJson.java
@@ -127,7 +127,9 @@ public class PhasePriceOverrideJson {
                     return new DefaultPlanPhasePriceOverride(input.getPhaseName(), currency, input.getFixedPrice(), input.getRecurringPrice());
                 } else {
                     final PhaseType phaseType = input.getPhaseType() != null ? PhaseType.valueOf(input.getPhaseType()) : null;
-                    final PlanPhaseSpecifier planPhaseSpecifier = new PlanPhaseSpecifier(spec.getProductName(), spec.getBillingPeriod(), spec.getPriceListName(), phaseType);
+                    final PlanPhaseSpecifier planPhaseSpecifier = spec.getPlanName() != null ?
+                                                                  new PlanPhaseSpecifier(spec.getPlanName(), phaseType) :
+                                                                  new PlanPhaseSpecifier(spec.getProductName(), spec.getBillingPeriod(), spec.getPriceListName(), phaseType);
                     return new DefaultPlanPhasePriceOverride(planPhaseSpecifier, currency, input.getFixedPrice(), input.getRecurringPrice());
                 }
             }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
index 5440c95..fd64e0a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
@@ -63,6 +63,8 @@ public class SubscriptionJson extends JsonBase {
     private final String phaseType;
     @ApiModelProperty(required = true)
     private final String priceList;
+    @ApiModelProperty(required = true)
+    private final String planName;
     //@ApiModelProperty(dataType = "org.killbill.billing.entitlement.api.Entitlement.EntitlementState")
     @ApiModelProperty(dataType = "string", allowableValues = "PENDING,ACTIVE,BLOCKED,CANCELLED")
     private final String state;
@@ -291,6 +293,7 @@ public class SubscriptionJson extends JsonBase {
                             @JsonProperty("billingPeriod") @Nullable final String billingPeriod,
                             @JsonProperty("phaseType") @Nullable final String phaseType,
                             @JsonProperty("priceList") @Nullable final String priceList,
+                            @JsonProperty("planName") @Nullable final String planName,
                             @JsonProperty("state") @Nullable final String state,
                             @JsonProperty("sourceType") @Nullable final String sourceType,
                             @JsonProperty("cancelledDate") @Nullable final LocalDate cancelledDate,
@@ -308,6 +311,7 @@ public class SubscriptionJson extends JsonBase {
         this.billingPeriod = billingPeriod;
         this.phaseType = phaseType;
         this.priceList = priceList;
+        this.planName = planName;
         this.state = state;
         this.sourceType = sourceType;
         this.cancelledDate = cancelledDate;
@@ -354,6 +358,12 @@ public class SubscriptionJson extends JsonBase {
         } else {
             this.priceList = subscription.getLastActivePriceList().getName();
         }
+        if (subscription.getLastActivePlan() == null) {
+            this.planName = firstEvent == null ? null : firstEvent.getNextPlan().getName();
+        } else {
+            this.planName = subscription.getLastActivePlan().getName();
+        }
+
 
         this.state = subscription.getState().name();
         this.sourceType = subscription.getSourceType().name();
@@ -426,6 +436,10 @@ public class SubscriptionJson extends JsonBase {
         return priceList;
     }
 
+    public String getPlanName() {
+        return planName;
+    }
+
     public String getState() {
         return state;
     }
@@ -475,6 +489,7 @@ public class SubscriptionJson extends JsonBase {
         sb.append(", billingPeriod='").append(billingPeriod).append('\'');
         sb.append(", phaseType='").append(phaseType).append('\'');
         sb.append(", priceList='").append(priceList).append('\'');
+        sb.append(", planName='").append(planName).append('\'');
         sb.append(", state='").append(state).append('\'');
         sb.append(", sourceType='").append(sourceType).append('\'');
         sb.append(", cancelledDate=").append(cancelledDate);
@@ -532,6 +547,9 @@ public class SubscriptionJson extends JsonBase {
         if (priceList != null ? !priceList.equals(that.priceList) : that.priceList != null) {
             return false;
         }
+        if (planName != null ? !planName.equals(that.planName) : that.planName != null) {
+            return false;
+        }
         if (productCategory != null ? !productCategory.equals(that.productCategory) : that.productCategory != null) {
             return false;
         }
@@ -571,6 +589,7 @@ public class SubscriptionJson extends JsonBase {
         result = 31 * result + (billingPeriod != null ? billingPeriod.hashCode() : 0);
         result = 31 * result + (phaseType != null ? phaseType.hashCode() : 0);
         result = 31 * result + (priceList != null ? priceList.hashCode() : 0);
+        result = 31 * result + (planName != null ? planName.hashCode() : 0);
         result = 31 * result + (state != null ? state.hashCode() : 0);
         result = 31 * result + (sourceType != null ? sourceType.hashCode() : 0);
         result = 31 * result + (cancelledDate != null ? cancelledDate.hashCode() : 0);
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 563e877..837fb6a 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
@@ -173,10 +173,12 @@ public class SubscriptionResource extends JaxRsResourceBase {
                                       @javax.ws.rs.core.Context final HttpServletRequest request,
                                       @javax.ws.rs.core.Context final UriInfo uriInfo) throws EntitlementApiException, AccountApiException, SubscriptionApiException {
         verifyNonNullOrEmpty(entitlement, "SubscriptionJson body should be specified");
-        verifyNonNullOrEmpty(entitlement.getProductName(), "SubscriptionJson productName needs to be set",
-                             entitlement.getProductCategory(), "SubscriptionJson productCategory needs to be set",
-                             entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set",
-                             entitlement.getPriceList(), "SubscriptionJson priceList needs to be set");
+        if (entitlement.getPlanName() == null) {
+            verifyNonNullOrEmpty(entitlement.getProductName(), "SubscriptionJson productName needs to be set",
+                                 entitlement.getProductCategory(), "SubscriptionJson productCategory needs to be set",
+                                 entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set",
+                                 entitlement.getPriceList(), "SubscriptionJson priceList needs to be set");
+        }
 
         logDeprecationParameterWarningIfNeeded(QUERY_REQUESTED_DT, QUERY_ENTITLEMENT_REQUESTED_DT, QUERY_BILLING_REQUESTED_DT);
 
@@ -195,7 +197,9 @@ public class SubscriptionResource extends JaxRsResourceBase {
 
                 final Account account = getAccountFromSubscriptionJson(entitlement, callContext);
                 final PhaseType phaseType = entitlement.getPhaseType() != null ? PhaseType.valueOf(entitlement.getPhaseType()) : null;
-                final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(entitlement.getProductName(),
+                final PlanPhaseSpecifier spec = entitlement.getPlanName() != null ?
+                                                new PlanPhaseSpecifier(entitlement.getPlanName(), phaseType) :
+                                                new PlanPhaseSpecifier(entitlement.getProductName(),
                                                                        BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), phaseType);
 
                 final LocalDate resolvedEntitlementDate = requestedDate != null ? toLocalDate(requestedDate) : toLocalDate(entitlementDate);
@@ -263,10 +267,12 @@ public class SubscriptionResource extends JaxRsResourceBase {
 
         for (SubscriptionJson entitlement : entitlements) {
             verifyNonNullOrEmpty(entitlement, "SubscriptionJson body should be specified for each element");
-            verifyNonNullOrEmpty(entitlement.getProductName(), "SubscriptionJson productName needs to be set for each element",
-                                 entitlement.getProductCategory(), "SubscriptionJson productCategory needs to be set for each element",
-                                 entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set for each element",
-                                 entitlement.getPriceList(), "SubscriptionJson priceList needs to be set for each element");
+            if (entitlement.getPlanName() == null) {
+                verifyNonNullOrEmpty(entitlement.getProductName(), "SubscriptionJson productName needs to be set for each element",
+                                     entitlement.getProductCategory(), "SubscriptionJson productCategory needs to be set for each element",
+                                     entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set for each element",
+                                     entitlement.getPriceList(), "SubscriptionJson priceList needs to be set for each element");
+            }
         }
 
         logDeprecationParameterWarningIfNeeded(QUERY_REQUESTED_DT, QUERY_ENTITLEMENT_REQUESTED_DT, QUERY_BILLING_REQUESTED_DT);
@@ -309,12 +315,11 @@ public class SubscriptionResource extends JaxRsResourceBase {
 
                 for (final SubscriptionJson entitlement : entitlements) {
 
-                    final PlanPhaseSpecifier planPhaseSpecifier = new PlanPhaseSpecifier(entitlement.getProductName(),
+                    final PlanPhaseSpecifier planPhaseSpecifier = entitlement.getPlanName() != null ?
+                                                                  new PlanPhaseSpecifier(entitlement.getPlanName(), null) :
+                                                                  new PlanPhaseSpecifier(entitlement.getProductName(),
                                                                                          BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), null);
-
-                    final PlanSpecifier planSpec = new PlanSpecifier(entitlement.getProductName(),
-                                                                     BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList());
-                    final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(entitlement.getPriceOverrides(), planSpec, account.getCurrency());
+                    final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(entitlement.getPriceOverrides(), planPhaseSpecifier, account.getCurrency());
 
                     EntitlementSpecifier specifier = new EntitlementSpecifier() {
 
@@ -394,9 +399,11 @@ public class SubscriptionResource extends JaxRsResourceBase {
                                           @HeaderParam(HDR_COMMENT) final String comment,
                                           @javax.ws.rs.core.Context final HttpServletRequest request) throws EntitlementApiException, AccountApiException, SubscriptionApiException {
         verifyNonNullOrEmpty(entitlement, "SubscriptionJson body should be specified");
-        verifyNonNullOrEmpty(entitlement.getProductName(), "SubscriptionJson productName needs to be set",
-                             entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set",
-                             entitlement.getPriceList(), "SubscriptionJson priceList needs to be set");
+        if (entitlement.getPlanName() == null) {
+            verifyNonNullOrEmpty(entitlement.getProductName(), "SubscriptionJson productName needs to be set",
+                                 entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set",
+                                 entitlement.getPriceList(), "SubscriptionJson priceList needs to be set");
+        }
 
         final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
         final CallContext callContext = context.createContext(createdBy, reason, comment, request);
@@ -415,17 +422,19 @@ public class SubscriptionResource extends JaxRsResourceBase {
                 final Entitlement newEntitlement;
 
                 final Account account = accountUserApi.getAccountById(current.getAccountId(), callContext);
-                final PlanSpecifier planSpec = new PlanSpecifier(entitlement.getProductName(),
+                final PlanSpecifier planSpec = entitlement.getPlanName() != null ?
+                                               new PlanSpecifier(entitlement.getPlanName()) :
+                                               new PlanSpecifier(entitlement.getProductName(),
                                                                  BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList());
                 final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(entitlement.getPriceOverrides(), planSpec, account.getCurrency());
 
                 if (requestedDate == null && policyString == null) {
-                    newEntitlement = current.changePlan(entitlement.getProductName(), BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), overrides, pluginProperties, ctx);
+                    newEntitlement = current.changePlan(planSpec, overrides, pluginProperties, ctx);
                 } else if (policyString == null) {
-                    newEntitlement = current.changePlanWithDate(entitlement.getProductName(), BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), overrides, inputLocalDate, pluginProperties, ctx);
+                    newEntitlement = current.changePlanWithDate(planSpec, overrides, inputLocalDate, pluginProperties, ctx);
                 } else {
                     final BillingActionPolicy policy = BillingActionPolicy.valueOf(policyString.toUpperCase());
-                    newEntitlement = current.changePlanOverrideBillingPolicy(entitlement.getProductName(), BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), overrides, inputLocalDate, policy, pluginProperties, ctx);
+                    newEntitlement = current.changePlanOverrideBillingPolicy(planSpec, overrides, inputLocalDate, policy, pluginProperties, ctx);
                 }
                 isImmediateOp = newEntitlement.getLastActiveProduct().getName().equals(entitlement.getProductName()) &&
                                 newEntitlement.getLastActivePlan().getRecurringBillingPeriod() == BillingPeriod.valueOf(entitlement.getBillingPeriod()) &&
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
index b700827..3dcd46f 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
@@ -69,6 +69,7 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuiteNoDB {
                                                                    UUID.randomUUID().toString(),
                                                                    UUID.randomUUID().toString(),
                                                                    UUID.randomUUID().toString(),
+                                                                   UUID.randomUUID().toString(),
                                                                    new LocalDate(),
                                                                    new LocalDate(),
                                                                    new LocalDate(),
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
index 529fc5a..f9e58bb 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestEntitlementJsonWithEvents.java
@@ -76,6 +76,7 @@ public class TestEntitlementJsonWithEvents extends JaxrsTestSuiteNoDB {
                                                                                 UUID.randomUUID().toString(),
                                                                                 UUID.randomUUID().toString(),
                                                                                 UUID.randomUUID().toString(),
+                                                                                UUID.randomUUID().toString(),
                                                                                 new LocalDate(),
                                                                                 new LocalDate(),
                                                                                 new LocalDate(),

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index d8ac7bf..c39ebc4 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.124</version>
+        <version>0.125</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.17.3-SNAPSHOT</version>
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 c6c1855..dc345c6 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
@@ -31,6 +31,7 @@ import org.killbill.billing.catalog.api.PhaseType;
 import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanChangeResult;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
 import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
@@ -65,28 +66,25 @@ public interface SubscriptionBaseApiService {
             throws SubscriptionBaseApiException;
 
     // Return the effective date of the change
-    public DateTime dryRunChangePlan(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
-                                     String priceList, DateTime requestedDate, BillingActionPolicy policy, TenantContext context) throws SubscriptionBaseApiException;
+    public DateTime dryRunChangePlan(DefaultSubscriptionBase subscription, PlanSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, TenantContext context) throws SubscriptionBaseApiException;
 
     // Return the effective date of the change
-    public DateTime changePlan(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
-                               String priceList, List<PlanPhasePriceOverride> overrides, CallContext context)
+    public DateTime changePlan(DefaultSubscriptionBase subscription, PlanSpecifier spec, List<PlanPhasePriceOverride> overrides, CallContext context)
             throws SubscriptionBaseApiException;
 
     // Return the effective date of the change
-    public DateTime changePlanWithRequestedDate(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
-                                                String priceList, List<PlanPhasePriceOverride> overrides, DateTime requestedDate, CallContext context)
+    public DateTime changePlanWithRequestedDate(DefaultSubscriptionBase subscription, PlanSpecifier spec,
+                                                List<PlanPhasePriceOverride> overrides, DateTime requestedDate, CallContext context)
             throws SubscriptionBaseApiException;
 
     // Return the effective date of the change
-    public DateTime changePlanWithPolicy(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
-                                         String priceList, List<PlanPhasePriceOverride> overrides, BillingActionPolicy policy, CallContext context)
+    public DateTime changePlanWithPolicy(DefaultSubscriptionBase subscription, PlanSpecifier spec,
+                                         List<PlanPhasePriceOverride> overrides, BillingActionPolicy policy, CallContext context)
             throws SubscriptionBaseApiException;
 
     public int cancelAddOnsIfRequiredOnBasePlanEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent event, final CallContext context) throws CatalogApiException;
 
-    public PlanChangeResult getPlanChangeResult(final DefaultSubscriptionBase subscription, final String productName,
-                                                final BillingPeriod term, final String priceList, final DateTime effectiveDate, TenantContext context) throws SubscriptionBaseApiException;
+    public PlanChangeResult getPlanChangeResult(final DefaultSubscriptionBase subscription, PlanSpecifier spec, final DateTime effectiveDate, TenantContext context) throws SubscriptionBaseApiException;
 
     //
     // Lower level APIs for dryRun functionality
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index 2046509..fe63e13 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -48,7 +48,6 @@ import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverridesWithCallContext;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.catalog.api.PlanSpecifier;
-import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
 import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
@@ -146,7 +145,6 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     @Override
     public SubscriptionBase createSubscription(final UUID bundleId, final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDateWithMs, final boolean isMigrated, final InternalCallContext context) throws SubscriptionBaseApiException {
         try {
-            final String realPriceList = (spec.getPriceListName() == null) ? PriceListSet.DEFAULT_PRICELIST_NAME : spec.getPriceListName();
             final DateTime now = clock.getUTCNow();
             final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
             /*
@@ -159,11 +157,11 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
             final Catalog catalog = catalogService.getFullCatalog(true, true, context);
             final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(overrides, callContext);
 
-            final Plan plan = catalog.createOrFindPlan(new PlanSpecifier(spec.getProductName(), spec.getBillingPeriod(), realPriceList), overridesWithContext, effectiveDate);
+            final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveDate);
             final PlanPhase phase = plan.getAllPhases()[0];
             if (phase == null) {
                 throw new SubscriptionBaseError(String.format("No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
-                                                              spec.getProductName(), spec.getBillingPeriod().toString(), realPriceList));
+                                                              spec.getProductName(), spec.getBillingPeriod().toString(), plan.getPriceListName()));
             }
 
             final SubscriptionBaseBundle bundle = dao.getSubscriptionBundleFromId(bundleId, context);
@@ -180,7 +178,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                                                  .setBundleStartDate(bundleStartDate)
                                                  .setAlignStartDate(effectiveDate)
                                                  .setMigrated(isMigrated),
-                                         plan, spec.getPhaseType(), realPriceList, effectiveDate, now, callContext);
+                                         plan, spec.getPhaseType(), plan.getPriceListName(), effectiveDate, now, callContext);
         } catch (final CatalogApiException e) {
             throw new SubscriptionBaseApiException(e);
         }
@@ -206,15 +204,14 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
             for (EntitlementSpecifier entitlement : entitlements) {
 
                 final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
-                final String realPriceList = (spec.getPriceListName() == null) ? PriceListSet.DEFAULT_PRICELIST_NAME : spec.getPriceListName();
 
                 final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(entitlement.getOverrides(), callContext);
 
-                final Plan plan = catalog.createOrFindPlan(new PlanSpecifier(spec.getProductName(), spec.getBillingPeriod(), realPriceList), overridesWithContext, effectiveDate);
+                final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveDate);
                 final PlanPhase phase = plan.getAllPhases()[0];
                 if (phase == null) {
                     throw new SubscriptionBaseError(String.format("No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
-                                                                  spec.getProductName(), spec.getBillingPeriod().toString(), realPriceList));
+                                                                  spec.getProductName(), spec.getBillingPeriod().toString(), plan.getPriceListName()));
                 }
 
                 if (first) {
@@ -225,7 +222,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                 }
 
                 SubscriptionSpecifier subscription = new SubscriptionSpecifier();
-                subscription.setRealPriceList(realPriceList);
+                subscription.setRealPriceList(plan.getPriceListName());
                 subscription.setEffectiveDate(effectiveDate);
                 subscription.setProcessedDate(now);
                 subscription.setPlan(plan);
@@ -464,14 +461,12 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
 
     @Override
     public DateTime getDryRunChangePlanEffectiveDate(final SubscriptionBase subscription,
-                                                     final String productName,
-                                                     final BillingPeriod term,
-                                                     final String priceList,
+                                                     final PlanSpecifier spec,
                                                      final DateTime requestedDateWithMs,
                                                      final BillingActionPolicy requestedPolicy,
                                                      final InternalTenantContext context) throws SubscriptionBaseApiException {
         final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);
-        return apiService.dryRunChangePlan((DefaultSubscriptionBase) subscription, productName, term, priceList, requestedDateWithMs, requestedPolicy, tenantContext);
+        return apiService.dryRunChangePlan((DefaultSubscriptionBase) subscription, spec, requestedDateWithMs, requestedPolicy, tenantContext);
     }
 
     @Override
@@ -539,12 +534,11 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
         List<SubscriptionBaseEvent> dryRunEvents = null;
         try {
             final PlanPhaseSpecifier inputSpec = dryRunArguments.getPlanPhaseSpecifier();
-            final String realPriceList = (inputSpec != null && inputSpec.getPriceListName() != null) ? inputSpec.getPriceListName() : PriceListSet.DEFAULT_PRICELIST_NAME;
             final Catalog catalog = catalogService.getFullCatalog(true, true, context);
 
             final PlanPhasePriceOverridesWithCallContext overridesWithContext = null; // TODO not supported to dryRun with custom price
             final Plan plan = (inputSpec != null && inputSpec.getProductName() != null && inputSpec.getBillingPeriod() != null) ?
-                              catalog.createOrFindPlan(new PlanSpecifier(inputSpec.getProductName(), inputSpec.getBillingPeriod(), realPriceList), overridesWithContext, utcNow) : null;
+                              catalog.createOrFindPlan(inputSpec, overridesWithContext, utcNow) : null;
             final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);
 
             if (dryRunArguments != null) {
@@ -555,7 +549,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                         final DateTime startEffectiveDate = dryRunArguments.getEffectiveDate() != null ? context.toUTCDateTime(dryRunArguments.getEffectiveDate()) : utcNow;
                         final DateTime bundleStartDate = getBundleStartDateWithSanity(bundleId, baseSubscription, plan, startEffectiveDate, context);
                         final UUID subscriptionId = UUIDs.randomUUID();
-                        dryRunEvents = apiService.getEventsOnCreation(bundleId, subscriptionId, startEffectiveDate, bundleStartDate, plan, inputSpec.getPhaseType(), realPriceList,
+                        dryRunEvents = apiService.getEventsOnCreation(bundleId, subscriptionId, startEffectiveDate, bundleStartDate, plan, inputSpec.getPhaseType(), plan.getPriceListName(),
                                                                       startEffectiveDate, utcNow, context);
                         final SubscriptionBuilder builder = new SubscriptionBuilder()
                                 .setId(subscriptionId)
@@ -574,15 +568,12 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                         if (changeEffectiveDate == null) {
                             BillingActionPolicy policy = dryRunArguments.getBillingActionPolicy();
                             if (policy == null) {
-                                final PlanChangeResult planChangeResult = apiService.getPlanChangeResult(subscriptionForChange,
-                                                                                                         dryRunArguments.getPlanPhaseSpecifier().getProductName(),
-                                                                                                         dryRunArguments.getPlanPhaseSpecifier().getBillingPeriod(),
-                                                                                                         dryRunArguments.getPlanPhaseSpecifier().getPriceListName(), utcNow, tenantContext);
+                                final PlanChangeResult planChangeResult = apiService.getPlanChangeResult(subscriptionForChange, inputSpec, utcNow, tenantContext);
                                 policy = planChangeResult.getPolicy();
                             }
                             changeEffectiveDate = subscriptionForChange.getPlanChangeEffectiveDate(policy);
                         }
-                        dryRunEvents = apiService.getEventsOnChangePlan(subscriptionForChange, plan, realPriceList, changeEffectiveDate, utcNow, true, context);
+                        dryRunEvents = apiService.getEventsOnChangePlan(subscriptionForChange, plan, plan.getPriceListName(), changeEffectiveDate, utcNow, true, context);
                         break;
 
                     case STOP_BILLING:
@@ -706,13 +697,12 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
             requestedDate = null;
         } else if (bcd <= lastDayOfMonth) {
             requestedDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), bcd);
-        } else /* bcd > lastDayOfMonth && bcd > currentDay */{
+        } else /* bcd > lastDayOfMonth && bcd > currentDay */ {
             requestedDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), lastDayOfMonth);
         }
         return requestedDate == null ? clock.getUTCNow() : internalCallContext.toUTCDateTime(requestedDate);
     }
 
-
     private DateTime getBundleStartDateWithSanity(final UUID bundleId, @Nullable final DefaultSubscriptionBase baseSubscription, final Plan plan,
                                                   final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException {
         switch (plan.getProduct().getCategory()) {
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 b3f06f2..5251d1b 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
@@ -35,6 +35,7 @@ import org.killbill.billing.catalog.api.PhaseType;
 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.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceList;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.catalog.api.ProductCategory;
@@ -241,21 +242,21 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
     }
 
     @Override
-    public DateTime changePlan(final String productName, final BillingPeriod term, final String priceList,
+    public DateTime changePlan(final PlanSpecifier spec,
                                final List<PlanPhasePriceOverride> overrides, final CallContext context) throws SubscriptionBaseApiException {
-        return apiService.changePlan(this, productName, term, priceList, overrides, context);
+        return apiService.changePlan(this, spec, overrides, context);
     }
 
     @Override
-    public DateTime changePlanWithDate(final String productName, final BillingPeriod term, final String priceList, final List<PlanPhasePriceOverride> overrides,
+    public DateTime changePlanWithDate(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides,
                                        final DateTime requestedDate, final CallContext context) throws SubscriptionBaseApiException {
-        return apiService.changePlanWithRequestedDate(this, productName, term, priceList, overrides, requestedDate, context);
+        return apiService.changePlanWithRequestedDate(this, spec, overrides, requestedDate, context);
     }
 
     @Override
-    public DateTime changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+    public DateTime changePlanWithPolicy(final PlanSpecifier spec,
                                          final List<PlanPhasePriceOverride> overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
-        return apiService.changePlanWithPolicy(this, productName, term, priceList, overrides, policy, context);
+        return apiService.changePlanWithPolicy(this, spec, overrides, policy, context);
     }
 
     @Override
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 64cc8d9..a814c2a 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
@@ -299,9 +299,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
 
     @Override
     public DateTime dryRunChangePlan(final DefaultSubscriptionBase subscription,
-                                     final String productName,
-                                     final BillingPeriod term,
-                                     final String priceList,
+                                     final PlanSpecifier spec,
                                      @Nullable final DateTime requestedDateWithMs,
                                      @Nullable final BillingActionPolicy requestedPolicy,
                                      final TenantContext context) throws SubscriptionBaseApiException {
@@ -309,7 +307,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
 
         BillingActionPolicy policyMaybeNull = requestedPolicy;
         if (requestedDateWithMs == null && requestedPolicy == null) {
-            final PlanChangeResult planChangeResult = getPlanChangeResult(subscription, productName, term, priceList, now, context);
+            final PlanChangeResult planChangeResult = getPlanChangeResult(subscription, spec, now, context);
             policyMaybeNull = planChangeResult.getPolicy();
         }
 
@@ -323,18 +321,17 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
     }
 
     @Override
-    public DateTime changePlan(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
-                               final String priceList, final List<PlanPhasePriceOverride> overrides, final CallContext context) throws SubscriptionBaseApiException {
+    public DateTime changePlan(final DefaultSubscriptionBase subscription, final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context) throws SubscriptionBaseApiException {
         final DateTime now = clock.getUTCNow();
 
         validateEntitlementState(subscription);
 
-        final PlanChangeResult planChangeResult = getPlanChangeResult(subscription, productName, term, priceList, now, context);
-        final DateTime effectiveDate = dryRunChangePlan(subscription, productName, term, priceList, null, planChangeResult.getPolicy(), context);
+        final PlanChangeResult planChangeResult = getPlanChangeResult(subscription, spec, now, context);
+        final DateTime effectiveDate = dryRunChangePlan(subscription, spec, null, planChangeResult.getPolicy(), context);
         validateEffectiveDate(subscription, effectiveDate);
 
         try {
-            doChangePlan(subscription, productName, term, planChangeResult.getNewPriceList().getName(), overrides, effectiveDate, context);
+            doChangePlan(subscription, spec, overrides, effectiveDate, context);
         } catch (final CatalogApiException e) {
             throw new SubscriptionBaseApiException(e);
         }
@@ -343,15 +340,14 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
     }
 
     @Override
-    public DateTime changePlanWithRequestedDate(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
-                                                final String priceList, final List<PlanPhasePriceOverride> overrides,
+    public DateTime changePlanWithRequestedDate(final DefaultSubscriptionBase subscription, final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides,
                                                 final DateTime requestedDateWithMs, final CallContext context) throws SubscriptionBaseApiException {
-        final DateTime effectiveDate = dryRunChangePlan(subscription, productName, term, priceList, requestedDateWithMs, null, context);
+        final DateTime effectiveDate = dryRunChangePlan(subscription, spec, requestedDateWithMs, null, context);
         validateEffectiveDate(subscription, effectiveDate);
         validateEntitlementState(subscription);
 
         try {
-            doChangePlan(subscription, productName, term, priceList, overrides, effectiveDate, context);
+            doChangePlan(subscription, spec, overrides, effectiveDate, context);
         } catch (final CatalogApiException e) {
             throw new SubscriptionBaseApiException(e);
         }
@@ -360,13 +356,12 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
     }
 
     @Override
-    public DateTime changePlanWithPolicy(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
-                                         final String priceList, final List<PlanPhasePriceOverride> overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+    public DateTime changePlanWithPolicy(final DefaultSubscriptionBase subscription, final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
         validateEntitlementState(subscription);
 
-        final DateTime effectiveDate = dryRunChangePlan(subscription, productName, term, priceList, null, policy, context);
+        final DateTime effectiveDate = dryRunChangePlan(subscription, spec, null, policy, context);
         try {
-            doChangePlan(subscription, productName, term, priceList, overrides, effectiveDate, context);
+            doChangePlan(subscription, spec, overrides, effectiveDate, context);
         } catch (final CatalogApiException e) {
             throw new SubscriptionBaseApiException(e);
         }
@@ -375,22 +370,17 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
     }
 
     @Override
-    public PlanChangeResult getPlanChangeResult(final DefaultSubscriptionBase subscription, final String productName,
-                                                final BillingPeriod term, final String priceList, final DateTime effectiveDate, final TenantContext context) throws SubscriptionBaseApiException {
+    public PlanChangeResult getPlanChangeResult(final DefaultSubscriptionBase subscription, final PlanSpecifier toPlanPhase, final DateTime effectiveDate, final TenantContext context) throws SubscriptionBaseApiException {
         final PlanChangeResult planChangeResult;
         try {
             final InternalTenantContext internalCallContext = createTenantContextFromBundleId(subscription.getBundleId(), context);
-            final Product destProduct = catalogService.getFullCatalog(true, true, internalCallContext).findProduct(productName, effectiveDate);
             final Plan currentPlan = subscription.getCurrentPlan();
+
             final PriceList currentPriceList = subscription.getCurrentPriceList();
             final PlanPhaseSpecifier fromPlanPhase = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
                                                                             currentPlan.getRecurringBillingPeriod(),
                                                                             currentPriceList.getName(),
                                                                             subscription.getCurrentPhase().getPhaseType());
-            final PlanSpecifier toPlanPhase = new PlanSpecifier(productName,
-                                                                term,
-                                                                priceList);
-
             planChangeResult = catalogService.getFullCatalog(true, true, internalCallContext).planChange(fromPlanPhase, toPlanPhase, effectiveDate);
         } catch (final CatalogApiException e) {
             throw new SubscriptionBaseApiException(e);
@@ -400,15 +390,13 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
     }
 
     private void doChangePlan(final DefaultSubscriptionBase subscription,
-                              final String newProductName,
-                              final BillingPeriod newBillingPeriod,
-                              final String newPriceList,
+                              final PlanSpecifier spec,
                               final List<PlanPhasePriceOverride> overrides,
                               final DateTime effectiveDate,
                               final CallContext context) throws SubscriptionBaseApiException, CatalogApiException {
         final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
         final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(overrides, context);
-        final Plan newPlan = catalogService.getFullCatalog(true, true, internalCallContext).createOrFindPlan(new PlanSpecifier(newProductName, newBillingPeriod, newPriceList), overridesWithContext, effectiveDate, subscription.getStartDate());
+        final Plan newPlan = catalogService.getFullCatalog(true, true, internalCallContext).createOrFindPlan(spec, overridesWithContext, effectiveDate, subscription.getStartDate());
 
         if (newPlan.getProduct().getCategory() != subscription.getCategory()) {
             throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_INVALID, subscription.getId());
@@ -416,7 +404,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
 
         final List<DefaultSubscriptionBase> addOnSubscriptionsToBeCancelled = new ArrayList<DefaultSubscriptionBase>();
         final List<SubscriptionBaseEvent> addOnCancelEvents = new ArrayList<SubscriptionBaseEvent>();
-        final List<SubscriptionBaseEvent> changeEvents = getEventsOnChangePlan(subscription, newPlan, newPriceList, effectiveDate, true, addOnSubscriptionsToBeCancelled, addOnCancelEvents, internalCallContext);
+        final List<SubscriptionBaseEvent> changeEvents = getEventsOnChangePlan(subscription, newPlan, newPlan.getPriceListName(), effectiveDate, true, addOnSubscriptionsToBeCancelled, addOnCancelEvents, internalCallContext);
 
         dao.changePlan(subscription, changeEvents, addOnSubscriptionsToBeCancelled, addOnCancelEvents, internalCallContext);
 
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java
index 664d8cb..bbbd1e4 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/transfer/TestTransfer.java
@@ -28,6 +28,7 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.PhaseType;
 import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
@@ -266,7 +267,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
         final String newBaseProduct1 = "Assault-Rifle";
         final BillingPeriod newBaseTerm1 = BillingPeriod.ANNUAL;
         testListener.pushExpectedEvent(NextEvent.CHANGE);
-        newBaseSubscription.changePlan(newBaseProduct1, newBaseTerm1, basePriceList, null, callContext);
+        newBaseSubscription.changePlan(new PlanSpecifier(newBaseProduct1, newBaseTerm1, basePriceList), null, callContext);
         assertListenerStatus();
 
         newPlan = newBaseSubscription.getCurrentPlan();
@@ -282,7 +283,7 @@ public class TestTransfer extends SubscriptionTestSuiteWithEmbeddedDB {
 
         final String newBaseProduct2 = "Pistol";
         final BillingPeriod newBaseTerm2 = BillingPeriod.ANNUAL;
-        newBaseSubscriptionWithCtd.changePlan(newBaseProduct2, newBaseTerm2, basePriceList, null, callContext);
+        newBaseSubscriptionWithCtd.changePlan(new PlanSpecifier(newBaseProduct2, newBaseTerm2, basePriceList), null, callContext);
 
         newPlan = newBaseSubscriptionWithCtd.getCurrentPlan();
         assertEquals(newPlan.getProduct().getName(), newBaseProduct1);
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 424475e..cb9e43a 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
@@ -311,7 +311,7 @@ public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
 
         testListener.pushExpectedEvent(NextEvent.CHANGE);
         testListener.pushExpectedEvent(NextEvent.CANCEL);
-        baseSubscription.changePlan(newBaseProduct, newBaseTerm, newBasePriceList, null, callContext);
+        baseSubscription.changePlan(new PlanSpecifier(newBaseProduct, newBaseTerm, newBasePriceList), null, callContext);
         assertListenerStatus();
 
         // REFETCH AO SUBSCRIPTION AND CHECK THIS CANCELLED
@@ -367,7 +367,7 @@ public class TestUserApiAddOn extends SubscriptionTestSuiteWithEmbeddedDB {
         assertEquals(aoStatus.get(0).getPriceList(), aoSubscription.getCurrentPriceList().getName());
         assertEquals(aoStatus.get(0).getReason(), DryRunChangeReason.AO_NOT_AVAILABLE_IN_NEW_PLAN);
 
-        baseSubscription.changePlan(newBaseProduct, newBaseTerm, newBasePriceList, null, callContext);
+        baseSubscription.changePlan(new PlanSpecifier(newBaseProduct, newBaseTerm, newBasePriceList), null, callContext);
 
         // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
         aoSubscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(aoSubscription.getId(), internalCallContext);
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 01a93e7..26eb6ff 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
@@ -23,6 +23,7 @@ import org.joda.time.DateTime;
 import org.joda.time.Interval;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.catalog.api.PlanAlignmentCreate;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -84,7 +85,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
 
             // CHANGE PLAN
             testListener.pushExpectedEvent(NextEvent.CHANGE);
-            subscription.changePlan(toProd, toTerm, toPlanSet, null, callContext);
+            subscription.changePlan(new PlanSpecifier(toProd, toTerm, toPlanSet), null, callContext);
             assertListenerStatus();
 
             // CHECK CHANGE PLAN
@@ -125,7 +126,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
 
         // RE READ SUBSCRIPTION + CHANGE PLAN
         subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
-        subscription.changePlan(toProd, toTerm, toPlanSet, null, callContext);
+        subscription.changePlan(new PlanSpecifier(toProd, toTerm, toPlanSet), null, callContext);
         assertListenerStatus();
 
         // CHECK CHANGE PLAN
@@ -168,7 +169,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
         clock.addDeltaFromReality(it.toDurationMillis());
 
         // CHANGE PLAN IMM
-        subscription.changePlan(toProd, toTerm, toPlanSet, null, callContext);
+        subscription.changePlan(new PlanSpecifier(toProd, toTerm, toPlanSet), null, callContext);
         checkChangePlan(subscription, toProd, ProductCategory.BASE, toTerm, PhaseType.TRIAL);
 
         assertListenerStatus();
@@ -222,7 +223,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
 
         // CHANGE PLAN
         currentTime = clock.getUTCNow();
-        subscription.changePlan(toProd, toTerm, toPlanSet, null, callContext);
+        subscription.changePlan(new PlanSpecifier(toProd, toTerm, toPlanSet), null, callContext);
 
         checkChangePlan(subscription, fromProd, ProductCategory.BASE, fromTerm, PhaseType.EVERGREEN);
 
@@ -288,12 +289,12 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
         subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
 
         // CHANGE EOT
-        subscription.changePlan("Pistol", BillingPeriod.MONTHLY, "gunclubDiscount", null, callContext);
+        subscription.changePlan(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, "gunclubDiscount"), null, callContext);
         assertListenerStatus();
 
         // CHANGE
         testListener.pushExpectedEvent(NextEvent.CHANGE);
-        subscription.changePlan("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount", null, callContext);
+        subscription.changePlan(new PlanSpecifier("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount"), null, callContext);
         assertListenerStatus();
 
         final Plan currentPlan = subscription.getCurrentPlan();
@@ -330,11 +331,11 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
         subscription = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
 
         // CHANGE EOT
-        subscription.changePlan("Shotgun", BillingPeriod.MONTHLY, "gunclubDiscount", null, callContext);
+        subscription.changePlan(new PlanSpecifier("Shotgun", BillingPeriod.MONTHLY, "gunclubDiscount"), null, callContext);
         assertListenerStatus();
 
         // CHANGE EOT
-        subscription.changePlan("Pistol", BillingPeriod.ANNUAL, "gunclubDiscount", null, callContext);
+        subscription.changePlan(new PlanSpecifier("Pistol", BillingPeriod.ANNUAL, "gunclubDiscount"), null, callContext);
         assertListenerStatus();
 
         // CHECK NO CHANGE OCCURED YET
@@ -396,7 +397,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
 
         // CHANGE IMMEDIATE TO A 3 PHASES PLAN
         testListener.pushExpectedEvent(NextEvent.CHANGE);
-        subscription.changePlan("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount", null, callContext);
+        subscription.changePlan(new PlanSpecifier("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount"), null, callContext);
         assertListenerStatus();
 
         // CHECK EVERYTHING LOOKS CORRECT
@@ -450,7 +451,7 @@ public class TestUserApiChangePlan extends SubscriptionTestSuiteWithEmbeddedDB {
         DefaultSubscriptionBase aoSubscription = testUtil.createSubscription(bundle, aoProduct, aoTerm, aoPriceList);
 
         try {
-            aoSubscription.changePlanWithDate(baseProduct, baseTerm, basePriceList, null, clock.getUTCNow(), callContext);
+            aoSubscription.changePlanWithDate(new PlanSpecifier(baseProduct, baseTerm, basePriceList), null, clock.getUTCNow(), callContext);
             Assert.fail("Should not allow plan change across product type");
         } catch (final SubscriptionBaseApiException e) {
             Assert.assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_INVALID.getCode());
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
index 46f3ab6..0c2e6c5 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
@@ -30,6 +30,7 @@ import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Duration;
 import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.subscription.SubscriptionTestSuiteNoDB;
 import org.killbill.billing.subscription.api.SubscriptionBase;
@@ -52,7 +53,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
         // TODO: MARTIN TO FIX WITH CORRECT ERROR CODE. RIGHT NOW NPE
 
         // WRONG BILLING PERIOD
-        tCreateSubscriptionInternal(bundle.getId(), "Shotgun", null, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.CAT_PLAN_NOT_FOUND);
+        tCreateSubscriptionInternal(bundle.getId(), "Shotgun", null, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.CAT_NULL_BILLING_PERIOD);
         // WRONG PLAN SET
         tCreateSubscriptionInternal(bundle.getId(), "Shotgun", BillingPeriod.ANNUAL, "Whatever", ErrorCode.CAT_PRICE_LIST_NOT_FOUND);
     }
@@ -106,7 +107,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
         testListener.pushExpectedEvent(NextEvent.CANCEL);
         subscription.cancelWithDate(clock.getUTCNow(), callContext);
         try {
-            subscription.changePlanWithDate("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, clock.getUTCNow(), callContext);
+            subscription.changePlanWithDate(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCNow(), callContext);
         } catch (final SubscriptionBaseApiException e) {
             assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_NON_ACTIVE.getCode());
         }
@@ -117,7 +118,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
         final SubscriptionBase subscription = testUtil.createSubscription(bundle, "Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME);
 
         try {
-            subscription.changePlanWithPolicy("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, BillingActionPolicy.ILLEGAL, callContext);
+            subscription.changePlanWithPolicy(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, BillingActionPolicy.ILLEGAL, callContext);
             Assert.fail();
         } catch (final SubscriptionBaseError error) {
             assertTrue(true);
@@ -125,7 +126,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
         }
 
         // Assume the call takes less than a second
-        assertEquals(DefaultClock.truncateMs(subscription.changePlanWithPolicy("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, BillingActionPolicy.IMMEDIATE, callContext)),
+        assertEquals(DefaultClock.truncateMs(subscription.changePlanWithPolicy(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, BillingActionPolicy.IMMEDIATE, callContext)),
                      DefaultClock.truncateMs(clock.getUTCNow()));
         assertEquals(subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext).getCurrentPlan().getRecurringBillingPeriod(), BillingPeriod.MONTHLY);
     }
@@ -152,7 +153,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
 
         subscription.cancelWithPolicy(BillingActionPolicy.END_OF_TERM, callContext);
         try {
-            subscription.changePlanWithDate("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null, clock.getUTCNow(), callContext);
+            subscription.changePlanWithDate(new PlanSpecifier("Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME), null, clock.getUTCNow(), callContext);
         } catch (final SubscriptionBaseApiException e) {
             assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_FUTURE_CANCELLED.getCode());
         }
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 6529062..c3287d1 100644
--- a/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
+++ b/util/src/test/java/org/killbill/billing/mock/MockSubscription.java
@@ -21,6 +21,7 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.mockito.Mockito;
 
 import org.killbill.billing.catalog.api.BillingActionPolicy;
@@ -81,20 +82,20 @@ public class MockSubscription implements SubscriptionBase {
     }
 
     @Override
-    public DateTime changePlan(final String productName, final BillingPeriod term, final String priceList, final List<PlanPhasePriceOverride> overrides, final CallContext context) throws SubscriptionBaseApiException {
-        return sub.changePlan(productName, term, priceList, overrides, context);
+    public DateTime changePlan(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context) throws SubscriptionBaseApiException {
+        return sub.changePlan(spec, overrides, context);
     }
 
     @Override
-    public DateTime changePlanWithDate(final String productName, final BillingPeriod term, final String priceList, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate,
+    public DateTime changePlanWithDate(final PlanSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate,
                                        final CallContext context) throws SubscriptionBaseApiException {
-        return sub.changePlanWithDate(productName, term, priceList, overrides, requestedDate, context);
+        return sub.changePlanWithDate(spec, overrides, requestedDate, context);
     }
 
     @Override
-    public DateTime changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+    public DateTime changePlanWithPolicy(final PlanSpecifier spec,
                                          final List<PlanPhasePriceOverride> overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
-        return sub.changePlanWithPolicy(productName, term, priceList, overrides, policy, context);
+        return sub.changePlanWithPolicy(spec, overrides, policy, context);
     }
 
     @Override