diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/alignment/PlanAligner.java b/entitlement/src/main/java/com/ning/billing/entitlement/alignment/PlanAligner.java
index 289927b..3593fea 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/alignment/PlanAligner.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/alignment/PlanAligner.java
@@ -63,13 +63,13 @@ public class PlanAligner {
/**
* Returns the current and next phase for the subscription in creation
*
- * @param subscription the subscription in creation
+ * @param subscription the subscription in creation (only the start date and the bundle start date are looked at)
* @param plan the current Plan
* @param initialPhase the initialPhase on which we should create that subscription. can be null
* @param priceList the priceList
- * @param requestedDate the requested date
- * @param effectiveDate the effective creation date
- * @return the current and next phase
+ * @param requestedDate the requested date (only used to load the catalog)
+ * @param effectiveDate the effective creation date (driven by the catalog policy, i.e. when the creation occurs)
+ * @return the current and next phases
* @throws CatalogApiException for catalog errors
* @throws EntitlementUserApiException for entitlement errors
*/
@@ -94,11 +94,12 @@ public class PlanAligner {
/**
* Returns current Phase for that Plan change
*
- * @param subscription the subscription in creation
+ * @param subscription the subscription in change (only start date, bundle start date, current phase, plan and pricelist
+ * are looked at)
* @param plan the current Plan
* @param priceList the priceList on which we should change that subscription.
* @param requestedDate the requested date
- * @param effectiveDate the effective change date
+ * @param effectiveDate the effective change date (driven by the catalog policy, i.e. when the change occurs)
* @return the current phase
* @throws CatalogApiException for catalog errors
* @throws EntitlementUserApiException for entitlement errors
@@ -114,11 +115,12 @@ public class PlanAligner {
/**
* Returns next Phase for that Plan change
*
- * @param subscription the subscription in creation
+ * @param subscription the subscription in change (only start date, bundle start date, current phase, plan and pricelist
+ * are looked at)
* @param plan the current Plan
* @param priceList the priceList on which we should change that subscription.
* @param requestedDate the requested date
- * @param effectiveDate the effective change date
+ * @param effectiveDate the effective change date (driven by the catalog policy, i.e. when the change occurs)
* @return the next phase
* @throws CatalogApiException for catalog errors
* @throws EntitlementUserApiException for entitlement errors
@@ -133,12 +135,11 @@ public class PlanAligner {
/**
* Returns next Phase for that Subscription at a point in time
- * <p/>
*
* @param subscription the subscription for which we need to compute the next Phase event
* @param requestedDate the requested date
* @param effectiveDate the date at which we look to compute that event. effective needs to be after last Plan change or initial Plan
- * @return The PhaseEvent at the correct point in time
+ * @return the next phase
*/
public TimedPhase getNextTimedPhase(final SubscriptionData subscription, final DateTime requestedDate, final DateTime effectiveDate) {
try {
@@ -160,7 +161,7 @@ public class PlanAligner {
lastPlanTransition.getNextPriceList().getName(),
requestedDate);
return getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
- // If we went through Plan changes, borrow the logics for changePlan alignment
+ // If we went through Plan changes, borrow the logic for changePlan alignment
case CHANGE:
return getTimedPhaseOnChange(subscription.getStartDate(),
subscription.getBundleStartDate(),
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 e162011..2c23f76 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
@@ -313,13 +313,16 @@ public class SubscriptionData extends EntityBase implements Subscription {
public SubscriptionTransitionData getInitialTransitionForCurrentPlan() {
if (transitions == null) {
- throw new EntitlementError(String.format(
- "No transitions for subscription %s", getId()));
+ throw new EntitlementError(String.format("No transitions for subscription %s", getId()));
}
- final SubscriptionTransitionDataIterator it = new SubscriptionTransitionDataIterator(
- clock, transitions, Order.DESC_FROM_FUTURE, Kind.ENTITLEMENT,
- Visibility.ALL, TimeLimit.PAST_OR_PRESENT_ONLY);
+ final SubscriptionTransitionDataIterator it = new SubscriptionTransitionDataIterator(clock,
+ transitions,
+ Order.DESC_FROM_FUTURE,
+ Kind.ENTITLEMENT,
+ Visibility.ALL,
+ TimeLimit.PAST_OR_PRESENT_ONLY);
+
while (it.hasNext()) {
final SubscriptionTransitionData cur = it.next();
if (cur.getTransitionType() == SubscriptionTransitionType.CREATE
@@ -329,9 +332,8 @@ public class SubscriptionData extends EntityBase implements Subscription {
return cur;
}
}
- throw new EntitlementError(String.format(
- "Failed to find InitialTransitionForCurrentPlan id = %s",
- getId().toString()));
+
+ throw new EntitlementError(String.format("Failed to find InitialTransitionForCurrentPlan id = %s", getId()));
}
public boolean isSubscriptionFutureCancelled() {
@@ -482,10 +484,9 @@ public class SubscriptionData extends EntityBase implements Subscription {
nextPhase = (nextPhaseName != null) ? catalog.findPhase(nextPhaseName, cur.getRequestedDate(), getStartDate()) : null;
nextPriceList = (nextPriceListName != null) ? catalog.findPriceList(nextPriceListName, cur.getRequestedDate()) : null;
} catch (CatalogApiException e) {
- log.error(String.format(
- "Failed to build transition for subscription %s", id),
- e);
+ log.error(String.format("Failed to build transition for subscription %s", id), e);
}
+
final SubscriptionTransitionData transition = new SubscriptionTransitionData(
cur.getId(), id, bundleId, cur.getType(), apiEventType,
cur.getRequestedDate(), cur.getEffectiveDate(),
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/alignment/TestPlanAligner.java b/entitlement/src/test/java/com/ning/billing/entitlement/alignment/TestPlanAligner.java
new file mode 100644
index 0000000..571630f
--- /dev/null
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/alignment/TestPlanAligner.java
@@ -0,0 +1,252 @@
+/*
+ * 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.entitlement.alignment;
+
+import java.util.List;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.ning.billing.catalog.DefaultCatalogService;
+import com.ning.billing.catalog.api.CatalogApiException;
+import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.io.VersionedCatalogLoader;
+import com.ning.billing.config.CatalogConfig;
+import com.ning.billing.entitlement.api.user.DefaultSubscriptionFactory;
+import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
+import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.entitlement.api.user.SubscriptionTransitionData;
+import com.ning.billing.entitlement.events.EntitlementEvent;
+import com.ning.billing.entitlement.events.user.ApiEventBase;
+import com.ning.billing.entitlement.events.user.ApiEventBuilder;
+import com.ning.billing.entitlement.events.user.ApiEventType;
+import com.ning.billing.entitlement.exceptions.EntitlementError;
+import com.ning.billing.util.clock.DefaultClock;
+
+public class TestPlanAligner {
+ private static final String priceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ private final DefaultClock clock = new DefaultClock();
+
+ private DefaultCatalogService catalogService;
+ private PlanAligner planAligner;
+
+ @BeforeClass(groups = "fast")
+ public void setUp() throws Exception {
+ final VersionedCatalogLoader versionedCatalogLoader = new VersionedCatalogLoader(clock);
+ final CatalogConfig config = new ConfigurationObjectFactory(new ConfigSource() {
+ final Map<String, String> properties = ImmutableMap.<String, String>of("killbill.catalog.uri", "file:src/test/resources/testInput.xml");
+
+ @Override
+ public String getString(final String propertyName) {
+ return properties.get(propertyName);
+ }
+ }).build(CatalogConfig.class);
+
+ catalogService = new DefaultCatalogService(config, versionedCatalogLoader);
+ planAligner = new PlanAligner(catalogService);
+
+ catalogService.loadCatalog();
+ }
+
+ @Test(groups = "fast")
+ public void testCreationBundleAlignment() throws Exception {
+ final String productName = "pistol-monthly";
+ final PhaseType initialPhase = PhaseType.TRIAL;
+ final SubscriptionData subscriptionData = createSubscriptionStartedInThePast(productName, initialPhase);
+
+ // Make the creation effective now, after the bundle and the subscription started
+ final DateTime effectiveDate = clock.getUTCNow();
+ final TimedPhase[] phases = getTimedPhasesOnCreate(productName, initialPhase, subscriptionData, effectiveDate);
+
+ // All plans but Laser-Scope are START_OF_BUNDLE aligned on creation
+ Assert.assertEquals(phases[0].getStartPhase(), subscriptionData.getBundleStartDate());
+ Assert.assertEquals(phases[1].getStartPhase(), subscriptionData.getBundleStartDate().plusDays(30));
+
+ // Verify the next phase via the other API
+ final TimedPhase nextTimePhase = planAligner.getNextTimedPhase(subscriptionData, effectiveDate, effectiveDate);
+ Assert.assertEquals(nextTimePhase.getStartPhase(), subscriptionData.getBundleStartDate().plusDays(30));
+
+ // Now look at the past, before the bundle started
+ final DateTime effectiveDateInThePast = subscriptionData.getBundleStartDate().minusHours(10);
+ final TimedPhase[] phasesInThePast = getTimedPhasesOnCreate(productName, initialPhase, subscriptionData, effectiveDateInThePast);
+ Assert.assertNull(phasesInThePast[0]);
+ Assert.assertEquals(phasesInThePast[1].getStartPhase(), subscriptionData.getBundleStartDate());
+
+ // Verify the next phase via the other API
+ try {
+ planAligner.getNextTimedPhase(subscriptionData, effectiveDateInThePast, effectiveDateInThePast);
+ Assert.fail("Can't use getNextTimedPhase(): the effective date is before the initial plan");
+ } catch (EntitlementError e) {
+ Assert.assertTrue(true);
+ }
+
+ // Try a change plan now (simulate an IMMEDIATE policy)
+ final String newProductName = "shotgun-monthly";
+ final DateTime effectiveChangeDate = clock.getUTCNow();
+ changeSubscription(effectiveChangeDate, subscriptionData, productName, newProductName, initialPhase);
+
+ // All non rescue plans are START_OF_SUBSCRIPTION aligned on change
+ final TimedPhase newPhase = getNextTimedPhaseOnChange(subscriptionData, newProductName, effectiveChangeDate);
+ Assert.assertEquals(newPhase.getStartPhase(), subscriptionData.getStartDate().plusDays(30),
+ String.format("Start phase: %s, but bundle start date: %s and subscription start date: %s",
+ newPhase.getStartPhase(), subscriptionData.getBundleStartDate(), subscriptionData.getStartDate()));
+ }
+
+ @Test(groups = "fast")
+ public void testCreationSubscriptionAlignment() throws Exception {
+ final String productName = "laser-scope-monthly";
+ final PhaseType initialPhase = PhaseType.DISCOUNT;
+ final SubscriptionData subscriptionData = createSubscriptionStartedInThePast(productName, initialPhase);
+
+ // Look now, after the bundle and the subscription started
+ final DateTime effectiveDate = clock.getUTCNow();
+ final TimedPhase[] phases = getTimedPhasesOnCreate(productName, initialPhase, subscriptionData, effectiveDate);
+
+ // Laser-Scope is START_OF_SUBSCRIPTION aligned on creation
+ Assert.assertEquals(phases[0].getStartPhase(), subscriptionData.getStartDate());
+ Assert.assertEquals(phases[1].getStartPhase(), subscriptionData.getStartDate().plusDays(30));
+
+ // Verify the next phase via the other API
+ final TimedPhase nextTimePhase = planAligner.getNextTimedPhase(subscriptionData, effectiveDate, effectiveDate);
+ Assert.assertEquals(nextTimePhase.getStartPhase(), subscriptionData.getStartDate().plusDays(30));
+
+ // Now look at the past, before the subscription started
+ final DateTime effectiveDateInThePast = subscriptionData.getStartDate().minusHours(10);
+ final TimedPhase[] phasesInThePast = getTimedPhasesOnCreate(productName, initialPhase, subscriptionData, effectiveDateInThePast);
+ Assert.assertNull(phasesInThePast[0]);
+ Assert.assertEquals(phasesInThePast[1].getStartPhase(), subscriptionData.getStartDate());
+
+ // Verify the next phase via the other API
+ try {
+ planAligner.getNextTimedPhase(subscriptionData, effectiveDateInThePast, effectiveDateInThePast);
+ Assert.fail("Can't use getNextTimedPhase(): the effective date is before the initial plan");
+ } catch (EntitlementError e) {
+ Assert.assertTrue(true);
+ }
+
+ // Try a change plan (simulate END_OF_TERM policy)
+ final String newProductName = "telescopic-scope-monthly";
+ final DateTime effectiveChangeDate = subscriptionData.getStartDate().plusDays(30);
+ changeSubscription(effectiveChangeDate, subscriptionData, productName, newProductName, initialPhase);
+
+ // All non rescue plans are START_OF_SUBSCRIPTION aligned on change. Since we're END_OF_TERM here, we'll
+ // never see the discount phase of telescopic-scope-monthly and jump right into evergreen.
+ // But in this test, since we didn't create the future change event from discount to evergreen (see changeSubscription,
+ // the subscription has only two transitions), we'll see null
+ final TimedPhase newPhase = getNextTimedPhaseOnChange(subscriptionData, newProductName, effectiveChangeDate);
+ Assert.assertNull(newPhase);
+ }
+
+ private SubscriptionData createSubscriptionStartedInThePast(final String productName, final PhaseType phaseType) {
+ final DefaultSubscriptionFactory.SubscriptionBuilder builder = new DefaultSubscriptionFactory.SubscriptionBuilder();
+ builder.setBundleStartDate(clock.getUTCNow().minusHours(10));
+ // Make sure to set the dates apart
+ builder.setStartDate(new DateTime(builder.getBundleStartDate().plusHours(5)));
+
+ // Create the transitions
+ final SubscriptionData subscriptionData = new SubscriptionData(builder, null, clock);
+ final EntitlementEvent event = createEntitlementEvent(builder.getStartDate(),
+ productName,
+ phaseType,
+ ApiEventType.CREATE,
+ subscriptionData.getActiveVersion());
+ subscriptionData.rebuildTransitions(ImmutableList.<EntitlementEvent>of(event), catalogService.getFullCatalog());
+
+ Assert.assertEquals(subscriptionData.getAllTransitions().size(), 1);
+ Assert.assertNull(subscriptionData.getAllTransitions().get(0).getPreviousPhase());
+ Assert.assertNotNull(subscriptionData.getAllTransitions().get(0).getNextPhase());
+
+ return subscriptionData;
+ }
+
+ private void changeSubscription(final DateTime effectiveChangeDate,
+ final SubscriptionData subscriptionData,
+ final String previousProductName,
+ final String newProductName,
+ final PhaseType commonPhaseType) {
+ final EntitlementEvent previousEvent = createEntitlementEvent(subscriptionData.getStartDate(),
+ previousProductName,
+ commonPhaseType,
+ ApiEventType.CREATE,
+ subscriptionData.getActiveVersion());
+ final EntitlementEvent event = createEntitlementEvent(effectiveChangeDate,
+ newProductName,
+ commonPhaseType,
+ ApiEventType.CHANGE,
+ subscriptionData.getActiveVersion());
+
+ subscriptionData.rebuildTransitions(ImmutableList.<EntitlementEvent>of(previousEvent, event), catalogService.getFullCatalog());
+
+ final List<SubscriptionTransitionData> newTransitions = subscriptionData.getAllTransitions();
+ Assert.assertEquals(newTransitions.size(), 2);
+ Assert.assertNull(newTransitions.get(0).getPreviousPhase());
+ Assert.assertEquals(newTransitions.get(0).getNextPhase(), newTransitions.get(1).getPreviousPhase());
+ Assert.assertNotNull(newTransitions.get(1).getNextPhase());
+ }
+
+ private EntitlementEvent createEntitlementEvent(final DateTime effectiveDate,
+ final String productName,
+ final PhaseType phaseType,
+ final ApiEventType apiEventType,
+ final long activeVersion) {
+ final ApiEventBuilder eventBuilder = new ApiEventBuilder();
+ eventBuilder.setEffectiveDate(effectiveDate);
+ eventBuilder.setEventPlan(productName);
+ eventBuilder.setEventPlanPhase(productName + "-" + phaseType.toString().toLowerCase());
+ eventBuilder.setEventPriceList(priceList);
+
+ // We don't really use the following but the code path requires it
+ eventBuilder.setRequestedDate(effectiveDate);
+ eventBuilder.setFromDisk(true);
+ eventBuilder.setActiveVersion(activeVersion);
+
+ return new ApiEventBase(eventBuilder.setEventType(apiEventType));
+ }
+
+ private TimedPhase getNextTimedPhaseOnChange(final SubscriptionData subscriptionData,
+ final String newProductName,
+ final DateTime effectiveChangeDate) throws CatalogApiException, EntitlementUserApiException {
+ // The date is used for different catalog versions - we don't care here
+ final Plan newPlan = catalogService.getFullCatalog().findPlan(newProductName, clock.getUTCNow());
+
+ return planAligner.getNextTimedPhaseOnChange(subscriptionData, newPlan, priceList, effectiveChangeDate, effectiveChangeDate);
+ }
+
+ private TimedPhase[] getTimedPhasesOnCreate(final String productName,
+ final PhaseType initialPhase,
+ final SubscriptionData subscriptionData,
+ final DateTime effectiveDate) throws CatalogApiException, EntitlementUserApiException {
+ // The date is used for different catalog versions - we don't care here
+ final Plan plan = catalogService.getFullCatalog().findPlan(productName, clock.getUTCNow());
+
+ // Same here for the requested date
+ final TimedPhase[] phases = planAligner.getCurrentAndNextTimedPhaseOnCreate(subscriptionData, plan, initialPhase, priceList, clock.getUTCNow(), effectiveDate);
+ Assert.assertEquals(phases.length, 2);
+
+ return phases;
+ }
+}