killbill-memoizeit

entitlement: add API call to override a policy for subscription

7/25/2012 6:48:34 PM

Details

diff --git a/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java b/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
index cb8993d..1e61a99 100644
--- a/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
+++ b/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
@@ -21,6 +21,7 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 
+import com.ning.billing.catalog.api.ActionPolicy;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
@@ -42,6 +43,10 @@ public interface Subscription extends Entity, Blockable {
     public boolean changePlan(String productName, BillingPeriod term, String priceList, DateTime requestedDate, CallContext context)
             throws EntitlementUserApiException;
 
+    public boolean changePlanWithPolicy(String productName, BillingPeriod term, String priceList, DateTime requestedDate,
+                                        ActionPolicy policy, CallContext context)
+            throws EntitlementUserApiException;
+
     public boolean recreate(PlanPhaseSpecifier spec, DateTime requestedDate, CallContext context)
             throws EntitlementUserApiException;
 
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java
new file mode 100644
index 0000000..57220ce
--- /dev/null
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestEntitlement.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.beatrix.integration;
+
+import org.joda.time.LocalDate;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.api.TestApiListener.NextEvent;
+import com.ning.billing.catalog.api.ActionPolicy;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.PlanPhaseSpecifier;
+import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.entitlement.api.user.SubscriptionData;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+@Guice(modules = {BeatrixModule.class})
+public class TestEntitlement extends TestIntegrationBase {
+
+    @Test(groups = "slow")
+    public void testForcePolicy() throws Exception {
+        // We take april as it has 30 days (easier to play with BCD)
+        final LocalDate today = new LocalDate(2012, 4, 1);
+        final Account account = createAccountWithPaymentMethod(getAccountData(1));
+
+        // Set clock to the initial start date - we implicitly assume here that the account timezone is UTC
+        clock.setDeltaFromReality(today.toDateTimeAtCurrentTime().getMillis() - clock.getUTCNow().getMillis());
+        final SubscriptionBundle bundle = entitlementUserApi.createBundleForAccount(account.getId(), "whatever", context);
+
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.ANNUAL;
+        final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+        //
+        // CREATE SUBSCRIPTION AND EXPECT BOTH EVENTS: NextEvent.CREATE NextEvent.INVOICE
+        //
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.INVOICE);
+        final PlanPhaseSpecifier bpPlanPhaseSpecifier = new PlanPhaseSpecifier(productName, ProductCategory.BASE, term, planSetName, null);
+        final SubscriptionData bpSubscription = subscriptionDataFromSubscription(entitlementUserApi.createSubscription(bundle.getId(),
+                                                                                                                       bpPlanPhaseSpecifier,
+                                                                                                                       null,
+                                                                                                                       context));
+        assertNotNull(bpSubscription);
+        assertTrue(busHandler.isCompleted(DELAY));
+        assertListenerStatus();
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId()).size(), 1);
+
+        assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId()).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+
+        // Move out of trials for interesting invoices adjustments
+        busHandler.pushExpectedEvent(NextEvent.PHASE);
+        clock.addDays(40);
+        assertTrue(busHandler.isCompleted(DELAY));
+        assertListenerStatus();
+
+        //
+        // FORCE AN IMMEDIATE CHANGE OF THE BILLING PERIOD
+        //
+        assertTrue(bpSubscription.changePlanWithPolicy(productName, BillingPeriod.MONTHLY, planSetName, clock.getUTCNow(), ActionPolicy.IMMEDIATE, context));
+        assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId()).getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
+
+        //
+        // FORCE ANOTHER CHANGE
+        //
+        assertTrue(bpSubscription.changePlanWithPolicy(productName, BillingPeriod.ANNUAL, planSetName, clock.getUTCNow(), ActionPolicy.IMMEDIATE, context));
+        assertEquals(entitlementUserApi.getSubscriptionFromId(bpSubscription.getId()).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+    }
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/SubscriptionApiService.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/SubscriptionApiService.java
index 8d216a2..1e35021 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/SubscriptionApiService.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/SubscriptionApiService.java
@@ -13,10 +13,12 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package com.ning.billing.entitlement.api;
 
 import org.joda.time.DateTime;
 
+import com.ning.billing.catalog.api.ActionPolicy;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.PhaseType;
 import com.ning.billing.catalog.api.Plan;
@@ -45,4 +47,8 @@ public interface SubscriptionApiService {
     public boolean changePlan(SubscriptionData subscription, String productName, BillingPeriod term,
                               String priceList, DateTime requestedDate, CallContext context)
             throws EntitlementUserApiException;
+
+    public boolean changePlanWithPolicy(SubscriptionData subscription, String productName, BillingPeriod term,
+                                        String priceList, DateTime requestedDate, ActionPolicy policy, CallContext context)
+            throws EntitlementUserApiException;
 }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultSubscriptionApiService.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultSubscriptionApiService.java
index 99e3e58..da61996 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultSubscriptionApiService.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultSubscriptionApiService.java
@@ -21,7 +21,6 @@ import java.util.List;
 
 import org.joda.time.DateTime;
 
-import com.google.inject.Inject;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.catalog.api.ActionPolicy;
 import com.ning.billing.catalog.api.BillingPeriod;
@@ -57,7 +56,10 @@ import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.clock.DefaultClock;
 
+import com.google.inject.Inject;
+
 public class DefaultSubscriptionApiService implements SubscriptionApiService {
+
     private final Clock clock;
     private final EntitlementDao dao;
     private final CatalogService catalogService;
@@ -71,6 +73,7 @@ public class DefaultSubscriptionApiService implements SubscriptionApiService {
         this.dao = dao;
     }
 
+    @Override
     public SubscriptionData createPlan(final SubscriptionBuilder builder, final Plan plan, final PhaseType initialPhase,
                                        final String realPriceList, final DateTime requestedDate, final DateTime effectiveDate, final DateTime processedDate,
                                        final CallContext context) throws EntitlementUserApiException {
@@ -80,6 +83,7 @@ public class DefaultSubscriptionApiService implements SubscriptionApiService {
         return subscription;
     }
 
+    @Override
     public boolean recreatePlan(final SubscriptionData subscription, final PlanPhaseSpecifier spec, final DateTime requestedDateWithMs, final CallContext context)
             throws EntitlementUserApiException {
         final SubscriptionState currentState = subscription.getState();
@@ -131,8 +135,8 @@ public class DefaultSubscriptionApiService implements SubscriptionApiService {
 
             final TimedPhase nextTimedPhase = curAndNextPhases[1];
             final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
-                    PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, processedDate, nextTimedPhase.getStartPhase()) :
-                    null;
+                                              PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, processedDate, nextTimedPhase.getStartPhase()) :
+                                              null;
             final List<EntitlementEvent> events = new ArrayList<EntitlementEvent>();
             events.add(creationEvent);
             if (nextPhaseEvent != null) {
@@ -149,6 +153,7 @@ public class DefaultSubscriptionApiService implements SubscriptionApiService {
         }
     }
 
+    @Override
     public boolean cancel(final SubscriptionData subscription, final DateTime requestedDateWithMs, final boolean eot, final CallContext context) throws EntitlementUserApiException {
         try {
             final SubscriptionState currentState = subscription.getState();
@@ -187,6 +192,7 @@ public class DefaultSubscriptionApiService implements SubscriptionApiService {
         }
     }
 
+    @Override
     public boolean uncancel(final SubscriptionData subscription, final CallContext context) throws EntitlementUserApiException {
         if (!subscription.isSubscriptionFutureCancelled()) {
             throw new EntitlementUserApiException(ErrorCode.ENT_UNCANCEL_BAD_STATE, subscription.getId().toString());
@@ -207,8 +213,8 @@ public class DefaultSubscriptionApiService implements SubscriptionApiService {
 
         final TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription, now, now);
         final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
-                PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
-                null;
+                                          PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+                                          null;
         if (nextPhaseEvent != null) {
             uncancelEvents.add(nextPhaseEvent);
         }
@@ -219,78 +225,108 @@ public class DefaultSubscriptionApiService implements SubscriptionApiService {
         return true;
     }
 
+    @Override
     public boolean changePlan(final SubscriptionData subscription, final String productName, final BillingPeriod term,
-                              final String priceList, final DateTime requestedDateWithMs, final CallContext context) throws EntitlementUserApiException {
-        try {
-            final DateTime now = clock.getUTCNow();
-            final DateTime requestedDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
-            validateRequestedDate(subscription, now, requestedDate);
+                              final String priceList, final DateTime requestedDateWithMs, final CallContext context)
+            throws EntitlementUserApiException {
+        final DateTime now = clock.getUTCNow();
+        final DateTime requestedDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
 
-            final PriceList currentPriceList = subscription.getCurrentPriceList();
+        validateRequestedDate(subscription, now, requestedDate);
+        validateSubscriptionState(subscription);
 
-            final SubscriptionState currentState = subscription.getState();
-            if (currentState != SubscriptionState.ACTIVE) {
-                throw new EntitlementUserApiException(ErrorCode.ENT_CHANGE_NON_ACTIVE, subscription.getId(), currentState);
-            }
+        final PlanChangeResult planChangeResult = getPlanChangeResult(subscription, productName, term, priceList, requestedDate);
+        final ActionPolicy policy = planChangeResult.getPolicy();
 
-            if (subscription.isSubscriptionFutureCancelled()) {
-                throw new EntitlementUserApiException(ErrorCode.ENT_CHANGE_FUTURE_CANCELLED, subscription.getId());
-            }
-            PlanChangeResult planChangeResult = null;
-            try {
-
-                final Product destProduct = catalogService.getFullCatalog().findProduct(productName, requestedDate);
-                final Plan currentPlan = subscription.getCurrentPlan();
-                final PlanPhaseSpecifier fromPlanPhase = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
-                                                                                currentPlan.getProduct().getCategory(),
-                                                                                currentPlan.getBillingPeriod(),
-                                                                                currentPriceList.getName(), subscription.getCurrentPhase().getPhaseType());
-                final PlanSpecifier toPlanPhase = new PlanSpecifier(productName,
-                                                                    destProduct.getCategory(),
-                                                                    term,
-                                                                    priceList);
-
-                planChangeResult = catalogService.getFullCatalog().planChange(fromPlanPhase, toPlanPhase, requestedDate);
-            } catch (CatalogApiException e) {
-                throw new EntitlementUserApiException(e);
-            }
+        try {
+            return doChangePlan(subscription, planChangeResult, now, requestedDate, productName, term, policy, context);
+        } catch (CatalogApiException e) {
+            throw new EntitlementUserApiException(e);
+        }
+    }
 
-            final ActionPolicy policy = planChangeResult.getPolicy();
-            final PriceList newPriceList = planChangeResult.getNewPriceList();
+    @Override
+    public boolean changePlanWithPolicy(final SubscriptionData subscription, final String productName, final BillingPeriod term,
+                                        final String priceList, final DateTime requestedDateWithMs, final ActionPolicy policy, final CallContext context)
+            throws EntitlementUserApiException {
+        final DateTime now = clock.getUTCNow();
+        final DateTime requestedDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
 
-            final Plan newPlan = catalogService.getFullCatalog().findPlan(productName, term, newPriceList.getName(), requestedDate, subscription.getStartDate());
-            final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, requestedDate);
+        validateRequestedDate(subscription, now, requestedDate);
+        validateSubscriptionState(subscription);
 
-            final TimedPhase currentTimedPhase = planAligner.getCurrentTimedPhaseOnChange(subscription, newPlan, newPriceList.getName(), requestedDate, effectiveDate);
+        final PlanChangeResult planChangeResult = getPlanChangeResult(subscription, productName, term, priceList, requestedDate);
 
-            final EntitlementEvent changeEvent = new ApiEventChange(new ApiEventBuilder()
-                                                                            .setSubscriptionId(subscription.getId())
-                                                                            .setEventPlan(newPlan.getName())
-                                                                            .setEventPlanPhase(currentTimedPhase.getPhase().getName())
-                                                                            .setEventPriceList(newPriceList.getName())
-                                                                            .setActiveVersion(subscription.getActiveVersion())
-                                                                            .setProcessedDate(now)
-                                                                            .setEffectiveDate(effectiveDate)
-                                                                            .setRequestedDate(requestedDate)
-                                                                            .setUserToken(context.getUserToken())
-                                                                            .setFromDisk(true));
+        try {
+            return doChangePlan(subscription, planChangeResult, now, requestedDate, productName, term, policy, context);
+        } catch (CatalogApiException e) {
+            throw new EntitlementUserApiException(e);
+        }
+    }
 
-            final TimedPhase nextTimedPhase = planAligner.getNextTimedPhaseOnChange(subscription, newPlan, newPriceList.getName(), requestedDate, effectiveDate);
-            final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
-                    PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
-                    null;
-            final List<EntitlementEvent> changeEvents = new ArrayList<EntitlementEvent>();
-            // Only add the PHASE if it does not coincide with the CHANGE, if not this is 'just' a CHANGE.
-            if (nextPhaseEvent != null && !nextPhaseEvent.getEffectiveDate().equals(changeEvent.getEffectiveDate())) {
-                changeEvents.add(nextPhaseEvent);
-            }
-            changeEvents.add(changeEvent);
-            dao.changePlan(subscription, changeEvents, context);
-            subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId()), catalogService.getFullCatalog());
-            return (policy == ActionPolicy.IMMEDIATE);
+    private PlanChangeResult getPlanChangeResult(final SubscriptionData subscription, final String productName,
+                                                 final BillingPeriod term, final String priceList, final DateTime requestedDate) throws EntitlementUserApiException {
+        final PlanChangeResult planChangeResult;
+        try {
+            final Product destProduct = catalogService.getFullCatalog().findProduct(productName, requestedDate);
+            final Plan currentPlan = subscription.getCurrentPlan();
+            final PriceList currentPriceList = subscription.getCurrentPriceList();
+            final PlanPhaseSpecifier fromPlanPhase = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
+                                                                            currentPlan.getProduct().getCategory(),
+                                                                            currentPlan.getBillingPeriod(),
+                                                                            currentPriceList.getName(),
+                                                                            subscription.getCurrentPhase().getPhaseType());
+            final PlanSpecifier toPlanPhase = new PlanSpecifier(productName,
+                                                                destProduct.getCategory(),
+                                                                term,
+                                                                priceList);
+
+            planChangeResult = catalogService.getFullCatalog().planChange(fromPlanPhase, toPlanPhase, requestedDate);
         } catch (CatalogApiException e) {
             throw new EntitlementUserApiException(e);
         }
+
+        return planChangeResult;
+    }
+
+    private boolean doChangePlan(final SubscriptionData subscription, final PlanChangeResult planChangeResult,
+                                 final DateTime now, final DateTime requestedDate, final String productName,
+                                 final BillingPeriod term, final ActionPolicy policy, final CallContext context) throws EntitlementUserApiException, CatalogApiException {
+        final PriceList newPriceList = planChangeResult.getNewPriceList();
+
+        final Plan newPlan = catalogService.getFullCatalog().findPlan(productName, term, newPriceList.getName(), requestedDate, subscription.getStartDate());
+        final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, requestedDate);
+
+        final TimedPhase currentTimedPhase = planAligner.getCurrentTimedPhaseOnChange(subscription, newPlan, newPriceList.getName(), requestedDate, effectiveDate);
+
+        final EntitlementEvent changeEvent = new ApiEventChange(new ApiEventBuilder()
+                                                                        .setSubscriptionId(subscription.getId())
+                                                                        .setEventPlan(newPlan.getName())
+                                                                        .setEventPlanPhase(currentTimedPhase.getPhase().getName())
+                                                                        .setEventPriceList(newPriceList.getName())
+                                                                        .setActiveVersion(subscription.getActiveVersion())
+                                                                        .setProcessedDate(now)
+                                                                        .setEffectiveDate(effectiveDate)
+                                                                        .setRequestedDate(requestedDate)
+                                                                        .setUserToken(context.getUserToken())
+                                                                        .setFromDisk(true));
+
+        final TimedPhase nextTimedPhase = planAligner.getNextTimedPhaseOnChange(subscription, newPlan, newPriceList.getName(), requestedDate, effectiveDate);
+        final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+                                          PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+                                          null;
+
+        final List<EntitlementEvent> changeEvents = new ArrayList<EntitlementEvent>();
+        // Only add the PHASE if it does not coincide with the CHANGE, if not this is 'just' a CHANGE.
+        if (nextPhaseEvent != null && !nextPhaseEvent.getEffectiveDate().equals(changeEvent.getEffectiveDate())) {
+            changeEvents.add(nextPhaseEvent);
+        }
+        changeEvents.add(changeEvent);
+
+        dao.changePlan(subscription, changeEvents, context);
+        subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId()), catalogService.getFullCatalog());
+
+        return (policy == ActionPolicy.IMMEDIATE);
     }
 
     private void validateRequestedDate(final SubscriptionData subscription, final DateTime now, final DateTime requestedDate)
@@ -306,4 +342,14 @@ public class DefaultSubscriptionApiService implements SubscriptionApiService {
                                                   requestedDate.toString(), previousTransition.getEffectiveTransitionTime());
         }
     }
+
+    private void validateSubscriptionState(final SubscriptionData subscription) throws EntitlementUserApiException {
+        final SubscriptionState currentState = subscription.getState();
+        if (currentState != SubscriptionState.ACTIVE) {
+            throw new EntitlementUserApiException(ErrorCode.ENT_CHANGE_NON_ACTIVE, subscription.getId(), currentState);
+        }
+        if (subscription.isSubscriptionFutureCancelled()) {
+            throw new EntitlementUserApiException(ErrorCode.ENT_CHANGE_FUTURE_CANCELLED, subscription.getId());
+        }
+    }
 }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
index 58fe83c..d7b8a78 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
@@ -176,11 +176,15 @@ public class SubscriptionData extends EntityBase implements Subscription {
     }
 
     @Override
-    public boolean changePlan(final String productName, final BillingPeriod term,
-                              final String priceList, final DateTime requestedDate, final CallContext context)
-            throws EntitlementUserApiException {
-        return apiService.changePlan(this, productName, term, priceList,
-                                     requestedDate, context);
+    public boolean changePlan(final String productName, final BillingPeriod term, final String priceList,
+                              final DateTime requestedDate, final CallContext context) throws EntitlementUserApiException {
+        return apiService.changePlan(this, productName, term, priceList, requestedDate, context);
+    }
+
+    @Override
+    public boolean changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+                                        final DateTime requestedDate, final ActionPolicy policy, final CallContext context) throws EntitlementUserApiException {
+        return apiService.changePlanWithPolicy(this, productName, term, priceList, requestedDate, policy, context);
     }
 
     @Override
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java
index afa82d0..adcddbc 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java
@@ -29,11 +29,13 @@ import com.google.inject.Injector;
 import com.google.inject.Stage;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.api.TestApiListener.NextEvent;
+import com.ning.billing.catalog.api.ActionPolicy;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Duration;
 import com.ning.billing.catalog.api.PlanPhase;
 import com.ning.billing.catalog.api.PriceListSet;
 import com.ning.billing.entitlement.api.TestApiBase;
+import com.ning.billing.entitlement.exceptions.EntitlementError;
 import com.ning.billing.entitlement.glue.MockEngineModuleMemory;
 import com.ning.billing.util.clock.DefaultClock;
 
@@ -59,7 +61,6 @@ public class TestUserApiError extends TestApiBase {
         tCreateSubscriptionInternal(bundle.getId(), "Shotgun", null, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.CAT_PLAN_NOT_FOUND);
         // WRONG PLAN SET
         tCreateSubscriptionInternal(bundle.getId(), "Shotgun", BillingPeriod.ANNUAL, "Whatever", ErrorCode.CAT_PRICE_LIST_NOT_FOUND);
-
     }
 
     @Test(groups = "fast")
@@ -162,6 +163,22 @@ public class TestUserApiError extends TestApiBase {
     }
 
     @Test(groups = "fast")
+    public void testChangeSubscriptionWithPolicy() throws Exception {
+        final Subscription subscription = createSubscription("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME);
+
+        try {
+            subscription.changePlanWithPolicy("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, clock.getUTCNow(), ActionPolicy.ILLEGAL, context);
+            Assert.fail();
+        } catch (EntitlementError error) {
+            assertTrue(true);
+            assertEquals(entitlementApi.getSubscriptionFromId(subscription.getId()).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
+        }
+
+        assertTrue(subscription.changePlanWithPolicy("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, clock.getUTCNow(), ActionPolicy.IMMEDIATE, context));
+        assertEquals(entitlementApi.getSubscriptionFromId(subscription.getId()).getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
+    }
+
+    @Test(groups = "fast")
     public void testChangeSubscriptionFutureCancelled() {
         try {
             Subscription subscription = createSubscription("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
index 984f633..095df0a 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
@@ -47,6 +47,7 @@ public interface JaxrsResource {
     public static final String QUERY_CALL_TIMEOUT = "call_timeout_sec";
     public static final String QUERY_DRY_RUN = "dry_run";
     public static final String QUERY_TARGET_DATE = "target_date";
+    public static final String QUERY_POLICY = "policy";
 
     public static final String QUERY_ACCOUNT_ID = "account_id";
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java
index 6851692..38027ff 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java
@@ -42,6 +42,7 @@ import org.joda.time.format.ISODateTimeFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.ning.billing.catalog.api.ActionPolicy;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.PlanPhaseSpecifier;
 import com.ning.billing.catalog.api.ProductCategory;
@@ -156,6 +157,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
                                            @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
                                            @QueryParam(QUERY_CALL_COMPLETION) @DefaultValue("false") final Boolean callCompletion,
                                            @QueryParam(QUERY_CALL_TIMEOUT) @DefaultValue("3") final long timeoutSec,
+                                           @QueryParam(QUERY_POLICY) final String policyString,
                                            @HeaderParam(HDR_CREATED_BY) final String createdBy,
                                            @HeaderParam(HDR_REASON) final String reason,
                                            @HeaderParam(HDR_COMMENT) final String comment) throws EntitlementUserApiException {
@@ -169,7 +171,16 @@ public class SubscriptionResource extends JaxRsResourceBase {
                 final UUID uuid = UUID.fromString(subscriptionId);
                 final Subscription current = entitlementApi.getSubscriptionFromId(uuid);
                 final DateTime inputDate = (requestedDate != null) ? DATE_TIME_FORMATTER.parseDateTime(requestedDate) : null;
-                isImmediateOp = current.changePlan(subscription.getProductName(), BillingPeriod.valueOf(subscription.getBillingPeriod()), subscription.getPriceList(), inputDate, ctx);
+
+                if (policyString == null) {
+                    isImmediateOp = current.changePlan(subscription.getProductName(), BillingPeriod.valueOf(subscription.getBillingPeriod()),
+                                                       subscription.getPriceList(), inputDate, ctx);
+                } else {
+                    final ActionPolicy policy = ActionPolicy.valueOf(policyString.toUpperCase());
+                    isImmediateOp = current.changePlanWithPolicy(subscription.getProductName(), BillingPeriod.valueOf(subscription.getBillingPeriod()),
+                                                                 subscription.getPriceList(), inputDate, policy, ctx);
+                }
+
                 return Response.status(Status.OK).build();
             }
 
diff --git a/junction/src/main/java/com/ning/billing/junction/plumbing/api/BlockingSubscription.java b/junction/src/main/java/com/ning/billing/junction/plumbing/api/BlockingSubscription.java
index bfce70b..3a7f932 100644
--- a/junction/src/main/java/com/ning/billing/junction/plumbing/api/BlockingSubscription.java
+++ b/junction/src/main/java/com/ning/billing/junction/plumbing/api/BlockingSubscription.java
@@ -21,6 +21,7 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 
+import com.ning.billing.catalog.api.ActionPolicy;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
@@ -65,14 +66,25 @@ public class BlockingSubscription implements Subscription {
     }
 
     @Override
-    public boolean changePlan(final String productName, final BillingPeriod term, final String planSet, final DateTime requestedDate,
+    public boolean changePlan(final String productName, final BillingPeriod term, final String priceList, final DateTime requestedDate,
                               final CallContext context) throws EntitlementUserApiException {
         try {
             checker.checkBlockedChange(this);
         } catch (BlockingApiException e) {
             throw new EntitlementUserApiException(e, e.getCode(), e.getMessage());
         }
-        return subscription.changePlan(productName, term, planSet, requestedDate, context);
+        return subscription.changePlan(productName, term, priceList, requestedDate, context);
+    }
+
+    @Override
+    public boolean changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+                                        final DateTime requestedDate, final ActionPolicy policy, final CallContext context) throws EntitlementUserApiException {
+        try {
+            checker.checkBlockedChange(this);
+        } catch (BlockingApiException e) {
+            throw new EntitlementUserApiException(e, e.getCode(), e.getMessage());
+        }
+        return subscription.changePlanWithPolicy(productName, term, priceList, requestedDate, policy, context);
     }
 
     @Override
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestSubscription.java b/server/src/test/java/com/ning/billing/jaxrs/TestSubscription.java
index ce1e9f4..da9b666 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestSubscription.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestSubscription.java
@@ -59,7 +59,7 @@ public class TestSubscription extends TestJaxrsBase {
         Assert.assertNotNull(subscriptionJson.getChargedThroughDate());
         Assert.assertEquals(subscriptionJson.getChargedThroughDate().toString(), "2012-04-25T00:00:00.000Z");
 
-        String uri = JaxrsResource.SUBSCRIPTIONS_PATH + "/" + subscriptionJson.getSubscriptionId().toString();
+        String uri = JaxrsResource.SUBSCRIPTIONS_PATH + "/" + subscriptionJson.getSubscriptionId();
 
         // Retrieves with GET
         Response response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
@@ -93,14 +93,13 @@ public class TestSubscription extends TestJaxrsBase {
 
         crappyWaitForLackOfProperSynchonization();
 
-        //
         // Cancel EOT
-        uri = JaxrsResource.SUBSCRIPTIONS_PATH + "/" + subscriptionJson.getSubscriptionId().toString();
+        uri = JaxrsResource.SUBSCRIPTIONS_PATH + "/" + subscriptionJson.getSubscriptionId();
         response = doDelete(uri, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
         assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
 
         // Retrieves to check EndDate
-        uri = JaxrsResource.SUBSCRIPTIONS_PATH + "/" + subscriptionJson.getSubscriptionId().toString();
+        uri = JaxrsResource.SUBSCRIPTIONS_PATH + "/" + subscriptionJson.getSubscriptionId();
         response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
 
         assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
@@ -132,4 +131,48 @@ public class TestSubscription extends TestJaxrsBase {
         Assert.assertEquals(response.getStatusCode(), Status.NOT_FOUND.getStatusCode());
     }
 
+    @Test(groups = "slow")
+    public void testOverridePolicy() throws Exception {
+        final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+        clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+        final AccountJson accountJson = createAccountWithDefaultPaymentMethod("xil", "shdxilhkkl", "xil@yahoo.com");
+        final BundleJsonNoSubscriptions bundleJson = createBundle(accountJson.getAccountId(), "99999");
+
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.ANNUAL;
+
+        final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), productName, ProductCategory.BASE.toString(), term.toString(), true);
+        Assert.assertNotNull(subscriptionJson.getChargedThroughDate());
+        Assert.assertEquals(subscriptionJson.getChargedThroughDate().toString(), "2012-04-25T00:00:00.000Z");
+
+        final String uri = JaxrsResource.SUBSCRIPTIONS_PATH + "/" + subscriptionJson.getSubscriptionId();
+
+        // Retrieves with GET
+        Response response = doGet(uri, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+        String baseJson = response.getResponseBody();
+        SubscriptionJsonNoEvents objFromJson = mapper.readValue(baseJson, SubscriptionJsonNoEvents.class);
+        Assert.assertTrue(objFromJson.equals(subscriptionJson));
+        assertEquals(objFromJson.getBillingPeriod(), BillingPeriod.ANNUAL.toString());
+
+        // Change billing period immediately
+        final SubscriptionJsonNoEvents newInput = new SubscriptionJsonNoEvents(subscriptionJson.getSubscriptionId(),
+                                                                               subscriptionJson.getBundleId(),
+                                                                               subscriptionJson.getStartDate(),
+                                                                               subscriptionJson.getProductName(),
+                                                                               subscriptionJson.getProductCategory(),
+                                                                               BillingPeriod.MONTHLY.toString(),
+                                                                               subscriptionJson.getPriceList(),
+                                                                               subscriptionJson.getChargedThroughDate(),
+                                                                               subscriptionJson.getCancelledDate());
+        baseJson = mapper.writeValueAsString(newInput);
+        final Map<String, String> queryParams = getQueryParamsForCallCompletion(CALL_COMPLETION_TIMEOUT_SEC);
+        queryParams.put(JaxrsResource.QUERY_POLICY, "immediate");
+        response = doPut(uri, baseJson, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
+        assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+        baseJson = response.getResponseBody();
+        objFromJson = mapper.readValue(baseJson, SubscriptionJsonNoEvents.class);
+        assertEquals(objFromJson.getBillingPeriod(), BillingPeriod.MONTHLY.toString());
+    }
 }
diff --git a/util/src/test/java/com/ning/billing/mock/MockSubscription.java b/util/src/test/java/com/ning/billing/mock/MockSubscription.java
index 2a3c024..013c9e0 100644
--- a/util/src/test/java/com/ning/billing/mock/MockSubscription.java
+++ b/util/src/test/java/com/ning/billing/mock/MockSubscription.java
@@ -24,6 +24,8 @@ import org.joda.time.DateTimeZone;
 import org.mockito.Mockito;
 
 import com.google.common.collect.ImmutableList;
+
+import com.ning.billing.catalog.api.ActionPolicy;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
@@ -78,9 +80,15 @@ public class MockSubscription implements Subscription {
     }
 
     @Override
-    public boolean changePlan(final String productName, final BillingPeriod term, final String planSet, final DateTime requestedDate,
+    public boolean changePlan(final String productName, final BillingPeriod term, final String priceList, final DateTime requestedDate,
                               final CallContext context) throws EntitlementUserApiException {
-        return sub.changePlan(productName, term, planSet, requestedDate, context);
+        return sub.changePlan(productName, term, priceList, requestedDate, context);
+    }
+
+    @Override
+    public boolean changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+                                        final DateTime requestedDate, final ActionPolicy policy, final CallContext context) throws EntitlementUserApiException {
+        return sub.changePlan(productName, term, priceList, requestedDate, context);
     }
 
     @Override