Details
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 85bf0fc..2903504 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
@@ -45,6 +45,7 @@ import com.ning.billing.entitlement.api.user.SubscriptionTransitionDataIterator.
import com.ning.billing.entitlement.api.user.SubscriptionTransitionDataIterator.TimeLimit;
import com.ning.billing.entitlement.api.user.SubscriptionTransitionDataIterator.Visibility;
import com.ning.billing.entitlement.events.EntitlementEvent;
+import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
import com.ning.billing.entitlement.events.phase.PhaseEvent;
import com.ning.billing.entitlement.events.user.ApiEvent;
import com.ning.billing.entitlement.events.user.ApiEventType;
@@ -379,8 +380,19 @@ public class SubscriptionData extends EntityBase implements Subscription {
final SubscriptionTransitionDataIterator it = new SubscriptionTransitionDataIterator(
clock, transitions, Order.ASC_FROM_PAST, Kind.BILLING,
Visibility.ALL, TimeLimit.ALL);
+ // Remove anything prior to first CREATE or MIGRATE_BILLING
+ boolean foundInitialEvent = false;
while (it.hasNext()) {
- result.add(it.next());
+ final SubscriptionTransitionData curTransition = it.next();
+ if (!foundInitialEvent) {
+ foundInitialEvent = curTransition.getEventType() == EventType.API_USER &&
+ (curTransition.getApiEventType() == ApiEventType.CREATE ||
+ curTransition.getApiEventType() == ApiEventType.MIGRATE_BILLING ||
+ curTransition.getApiEventType() == ApiEventType.TRANSFER);
+ }
+ if (foundInitialEvent) {
+ result.add(curTransition);
+ }
}
return result;
}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/DefaultEntitlementDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/DefaultEntitlementDao.java
index de613c3..d5eaf90 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/DefaultEntitlementDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/DefaultEntitlementDao.java
@@ -60,9 +60,12 @@ import com.ning.billing.entitlement.engine.dao.model.SubscriptionBundleModelDao;
import com.ning.billing.entitlement.engine.dao.model.SubscriptionModelDao;
import com.ning.billing.entitlement.events.EntitlementEvent;
import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
+import com.ning.billing.entitlement.events.phase.PhaseEvent;
+import com.ning.billing.entitlement.events.user.ApiEvent;
import com.ning.billing.entitlement.events.user.ApiEventBuilder;
import com.ning.billing.entitlement.events.user.ApiEventCancel;
import com.ning.billing.entitlement.events.user.ApiEventChange;
+import com.ning.billing.entitlement.events.user.ApiEventMigrateBilling;
import com.ning.billing.entitlement.events.user.ApiEventType;
import com.ning.billing.entitlement.exceptions.EntitlementError;
import com.ning.billing.util.callcontext.InternalCallContext;
@@ -481,12 +484,19 @@ public class DefaultEntitlementDao implements EntitlementDao {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
- final EntitlementEventSqlDao transactional = entitySqlDaoWrapperFactory.become(EntitlementEventSqlDao.class);
+ final EntitlementEventSqlDao transactional = entitySqlDaoWrapperFactory.become(EntitlementEventSqlDao.class);
final UUID subscriptionId = subscription.getId();
+
+ final List<EntitlementEvent> changeEventsTweakedWithMigrateBilling = reinsertFutureMigrateBillingEventOnChangeFromTransaction(subscriptionId,
+ changeEvents,
+ entitySqlDaoWrapperFactory,
+ context);
+
cancelFutureEventsFromTransaction(subscriptionId, entitySqlDaoWrapperFactory, context);
- for (final EntitlementEvent cur : changeEvents) {
+ for (final EntitlementEvent cur : changeEventsTweakedWithMigrateBilling) {
+
transactional.create(new EntitlementEventModelDao(cur), context);
recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory,
cur.getEffectiveDate(),
@@ -495,7 +505,7 @@ public class DefaultEntitlementDao implements EntitlementDao {
}
// Notify the Bus of the latest requested change
- final EntitlementEvent finalEvent = changeEvents.get(changeEvents.size() - 1);
+ final EntitlementEvent finalEvent = changeEventsTweakedWithMigrateBilling.get(changeEvents.size() - 1);
notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subscription, finalEvent, context);
return null;
@@ -503,6 +513,90 @@ public class DefaultEntitlementDao implements EntitlementDao {
});
}
+ //
+ // This piece of code has been isolated in its own method in order to allow for migrated subscriptions to have their plan to changed prior
+ // to MIGRATE_BILLING; the effect will be to reflect the change from an entitlement point of view while ignoring the change until we hit
+ // the begining of the billing, that when we hit the MIGRATE_BILLING event. If we had a clear separation between entitlement and
+ // billing that would not be needed.
+ //
+ // If there is a change of plan prior to a future MIGRATE_BILLING, we want to modify the existing MIGRATE_BILLING so it reflects
+ // the new plan, phase, pricelist; Invoice will only see the MIGRATE_BILLING as things prior to that will be ignored, so we need to make sure
+ // that event reflects the correct entitlement information.
+ //
+ //
+ final List<EntitlementEvent> reinsertFutureMigrateBillingEventOnChangeFromTransaction(final UUID subscriptionId, final List<EntitlementEvent> changeEvents, final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final InternalCallContext context) {
+ final EntitlementEventModelDao migrateBillingEvent = findFutureEventFromTransaction(subscriptionId, entitySqlDaoWrapperFactory, EventType.API_USER, ApiEventType.MIGRATE_BILLING, context);
+ if (migrateBillingEvent == null) {
+ // No future migrate billing : returns same list
+ return changeEvents;
+ }
+
+ String prevPlan = null;
+ String prevPhase = null;
+ String prevPriceList = null;
+ for (EntitlementEvent cur : changeEvents) {
+ switch (cur.getType()) {
+ case API_USER:
+ final ApiEvent apiEvent = (ApiEvent) cur;
+ prevPlan = apiEvent.getEventPlan();
+ prevPhase = apiEvent.getEventPlanPhase();
+ prevPriceList = apiEvent.getPriceList();
+ break;
+
+ case PHASE:
+ final PhaseEvent phaseEvent = (PhaseEvent) cur;
+ prevPhase = phaseEvent.getPhase();
+ break;
+
+ default:
+ throw new EntitlementError("Unknown event type " + cur.getType());
+ }
+
+ if (cur.getEffectiveDate().compareTo(migrateBillingEvent.getEffectiveDate()) > 0) {
+ if (cur.getType() == EventType.API_USER && ((ApiEvent) cur).getEventType() == ApiEventType.CHANGE) {
+ // This is an EOT change that is occurring after the MigrateBilling : returns same list
+ return changeEvents;
+ }
+ // We found the first event after the migrate billing
+ break;
+ }
+ }
+
+ if (prevPlan != null) {
+ // Create the new MIGRATE_BILLING with same effectiveDate but new plan information
+ final DateTime now = clock.getUTCNow();
+ final ApiEventBuilder builder = new ApiEventBuilder()
+ .setActive(true)
+ .setEventType(ApiEventType.MIGRATE_BILLING)
+ .setFromDisk(true)
+ .setTotalOrdering(migrateBillingEvent.getTotalOrdering())
+ .setUuid(UUID.randomUUID())
+ .setSubscriptionId(migrateBillingEvent.getSubscriptionId())
+ .setCreatedDate(now)
+ .setUpdatedDate(now)
+ .setRequestedDate(migrateBillingEvent.getRequestedDate())
+ .setEffectiveDate(migrateBillingEvent.getEffectiveDate())
+ .setProcessedDate(now)
+ .setActiveVersion(migrateBillingEvent.getCurrentVersion())
+ .setUserToken(context.getUserToken())
+ .setEventPlan(prevPlan)
+ .setEventPlanPhase(prevPhase)
+ .setEventPriceList(prevPriceList);
+
+ final EntitlementEvent newMigrateBillingEvent = new ApiEventMigrateBilling(builder);
+ changeEvents.add(newMigrateBillingEvent);
+
+ Collections.sort(changeEvents, new Comparator<EntitlementEvent>() {
+ @Override
+ public int compare(final EntitlementEvent o1, final EntitlementEvent o2) {
+ return o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
+ }
+ });
+ }
+
+ return changeEvents;
+ }
+
private void cancelSubscriptionFromTransaction(final SubscriptionData subscription, final EntitlementEvent cancelEvent, final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final InternalCallContext context, final int seqId)
throws EntityPersistenceException {
final UUID subscriptionId = subscription.getId();
@@ -531,6 +625,13 @@ public class DefaultEntitlementDao implements EntitlementDao {
private void cancelFutureEventFromTransaction(final UUID subscriptionId, final EntitySqlDaoWrapperFactory<EntitySqlDao> dao, final EventType type,
@Nullable final ApiEventType apiType, final InternalCallContext context) {
+ final EntitlementEventModelDao futureEvent = findFutureEventFromTransaction(subscriptionId, dao, type, apiType, context);
+ unactivateEventFromTransaction(futureEvent, dao, context);
+ }
+
+ private EntitlementEventModelDao findFutureEventFromTransaction(final UUID subscriptionId, final EntitySqlDaoWrapperFactory<EntitySqlDao> dao, final EventType type,
+ @Nullable final ApiEventType apiType, final InternalCallContext context) {
+
EntitlementEventModelDao futureEvent = null;
final Date now = clock.getUTCNow().toDate();
final List<EntitlementEventModelDao> eventModels = dao.become(EntitlementEventSqlDao.class).getFutureActiveEventForSubscription(subscriptionId.toString(), now, context);
@@ -542,9 +643,11 @@ public class DefaultEntitlementDao implements EntitlementDao {
type, subscriptionId.toString()));
}
futureEvent = cur;
+ // To check that there is only one such event
+ //break;
}
}
- unactivateEventFromTransaction(futureEvent, dao, context);
+ return futureEvent;
}
private void unactivateEventFromTransaction(final EntitlementEventModelDao event, final EntitySqlDaoWrapperFactory<EntitySqlDao> dao, final InternalCallContext context) {
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java
index aefdfbe..af41017 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java
@@ -26,8 +26,10 @@ import java.util.List;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.testng.Assert;
+import org.testng.annotations.Test;
import com.ning.billing.api.TestApiListener.NextEvent;
+import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.PhaseType;
import com.ning.billing.catalog.api.PriceListSet;
import com.ning.billing.catalog.api.ProductCategory;
@@ -36,6 +38,9 @@ import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi.Entitl
import com.ning.billing.entitlement.api.user.Subscription;
import com.ning.billing.entitlement.api.user.Subscription.SubscriptionState;
import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.entitlement.api.user.SubscriptionTransitionData;
+import com.ning.billing.entitlement.events.user.ApiEventType;
public abstract class TestMigration extends TestApiBase {
public void testSingleBasePlan() {
@@ -61,7 +66,7 @@ public abstract class TestMigration extends TestApiBase {
assertEquals(subscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
assertEquals(subscription.getState(), SubscriptionState.ACTIVE);
- assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-annual");
+ assertEquals(subscription.getCurrentPlan().getName(), "shotgun-annual");
assertEquals(subscription.getChargedThroughDate(), startDate.plusYears(1));
assertListenerStatus();
@@ -256,4 +261,63 @@ public abstract class TestMigration extends TestApiBase {
Assert.fail("", e);
}
}
+
+ public void testChangePriorMigrateBilling() throws Exception {
+ try {
+ final DateTime startDate = clock.getUTCNow().minusMonths(2);
+ final DateTime beforeMigration = clock.getUTCNow();
+ final EntitlementAccountMigration toBeMigrated = createAccountForMigrationWithRegularBasePlan(startDate);
+ final DateTime afterMigration = clock.getUTCNow();
+
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+ migrationApi.migrate(toBeMigrated, callContext);
+ assertTrue(testListener.isCompleted(5000));
+ assertListenerStatus();
+
+ final List<SubscriptionBundle> bundles = entitlementApi.getBundlesForAccount(toBeMigrated.getAccountKey(), callContext);
+ assertEquals(bundles.size(), 1);
+
+ final List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(bundles.get(0).getId(), callContext);
+ assertEquals(subscriptions.size(), 1);
+ final SubscriptionData subscription = (SubscriptionData) subscriptions.get(0);
+
+ final List<SubscriptionTransitionData> transitions = subscription.getAllTransitions();
+ assertEquals(transitions.size(), 2);
+ final SubscriptionTransitionData initialMigrateBilling = transitions.get(1);
+ assertEquals(initialMigrateBilling.getApiEventType(), ApiEventType.MIGRATE_BILLING);
+ assertTrue(initialMigrateBilling.getEffectiveTransitionTime().compareTo(subscription.getChargedThroughDate()) == 0);
+ assertEquals(initialMigrateBilling.getNextPlan().getName(), "shotgun-annual");
+ assertEquals(initialMigrateBilling.getNextPhase().getName(), "shotgun-annual-evergreen");
+
+ final List<SubscriptionTransitionData> billingTransitions = subscription.getBillingTransitions();
+ assertEquals(billingTransitions.size(), 1);
+ assertEquals(billingTransitions.get(0), initialMigrateBilling);
+
+ // Now make an IMMEDIATE change of plan
+ subscription.changePlan("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, clock.getUTCNow(), callContext);
+
+ final List<SubscriptionTransitionData> newTransitions = subscription.getAllTransitions();
+ assertEquals(newTransitions.size(), 3);
+
+ final SubscriptionTransitionData changeTransition = newTransitions.get(1);
+ assertEquals(changeTransition.getApiEventType(), ApiEventType.CHANGE);
+
+ final SubscriptionTransitionData newMigrateBilling = newTransitions.get(2);
+ assertEquals(newMigrateBilling.getApiEventType(), ApiEventType.MIGRATE_BILLING);
+ assertTrue(newMigrateBilling.getEffectiveTransitionTime().compareTo(subscription.getChargedThroughDate()) == 0);
+ assertTrue(newMigrateBilling.getEffectiveTransitionTime().compareTo(initialMigrateBilling.getEffectiveTransitionTime()) == 0);
+ assertEquals(newMigrateBilling.getNextPlan().getName(), "assault-rifle-monthly");
+ assertEquals(newMigrateBilling.getNextPhase().getName(), "assault-rifle-monthly-evergreen");
+
+
+ final List<SubscriptionTransitionData> newBillingTransitions = subscription.getBillingTransitions();
+ assertEquals(newBillingTransitions.size(), 1);
+ assertEquals(newBillingTransitions.get(0), newMigrateBilling);
+
+
+ } catch (EntitlementMigrationApiException e) {
+ Assert.fail("", e);
+ }
+
+ }
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java
index a9b15b0..275a285 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java
@@ -53,4 +53,10 @@ public class TestMigrationSql extends TestMigration {
public void testSingleBasePlanWithPendingPhase() {
super.testSingleBasePlanWithPendingPhase();
}
+
+ @Override
+ @Test(groups = "slow")
+ public void testChangePriorMigrateBilling() throws Exception {
+ super.testChangePriorMigrateBilling();
+ }
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestApiBase.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestApiBase.java
index d9e7103..0b95f47 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestApiBase.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestApiBase.java
@@ -495,7 +495,7 @@ public abstract class TestApiBase extends EntitlementTestSuiteWithEmbeddedDB imp
protected EntitlementAccountMigration createAccountForMigrationWithRegularBasePlan(final DateTime startDate) {
final List<EntitlementSubscriptionMigrationCaseWithCTD> cases = new LinkedList<EntitlementSubscriptionMigrationCaseWithCTD>();
cases.add(new EntitlementSubscriptionMigrationCaseWithCTD(
- new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN),
+ new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN),
startDate,
null,
startDate.plusYears(1)));
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestTransfer.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestTransfer.java
index ec61b63..ee43c8b 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestTransfer.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestTransfer.java
@@ -88,7 +88,7 @@ public class TestTransfer extends TestApiBase {
assertEquals(subscription.getCurrentPriceList().getName(), PriceListSet.DEFAULT_PRICELIST_NAME);
assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
assertEquals(subscription.getState(), SubscriptionState.ACTIVE);
- assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-annual");
+ assertEquals(subscription.getCurrentPlan().getName(), "shotgun-annual");
assertEquals(subscription.getChargedThroughDate(), startDate.plusYears(1));
// WE should see MIGRATE_ENTITLEMENT and then MIGRATE_BILLING in the future
assertEquals(entitlementInternalApi.getBillingTransitions(subscription, internalCallContext).size(), 1);
@@ -110,8 +110,8 @@ public class TestTransfer extends TestApiBase {
final Subscription oldBaseSubscription = entitlementApi.getBaseSubscription(bundle.getId(), callContext);
assertTrue(oldBaseSubscription.getState() == SubscriptionState.CANCELLED);
// The MIGRATE_BILLING event should have been invalidated
- assertEquals(entitlementInternalApi.getBillingTransitions(oldBaseSubscription, internalCallContext).size(), 1);
- assertEquals(entitlementInternalApi.getBillingTransitions(oldBaseSubscription, internalCallContext).get(0).getTransitionType(), SubscriptionTransitionType.CANCEL);
+ assertEquals(entitlementInternalApi.getBillingTransitions(oldBaseSubscription, internalCallContext).size(), 0);
+ //assertEquals(entitlementInternalApi.getBillingTransitions(oldBaseSubscription, internalCallContext).get(0).getTransitionType(), SubscriptionTransitionType.CANCEL);
} catch (EntitlementMigrationApiException e) {
Assert.fail("", e);