killbill-memoizeit

Implement a dirty fix to allow NCs to chnage their plans after

11/28/2012 4:58:10 PM

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);