killbill-memoizeit

Changes

Details

diff --git a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
index d1c1b5d..8e30d0c 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
@@ -92,6 +92,7 @@ import com.ning.billing.util.svcsapi.bus.InternalBus;
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 
 import static org.testng.Assert.fail;
@@ -254,7 +255,7 @@ public class TestAnalyticsService extends AnalyticsTestSuiteWithEmbeddedDB {
                                                                                                                                              }
                                                                                                                                          }));
 
-        invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, true, internalCallContext);
+        invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, true, ImmutableMap.<UUID, DateTime>of(), internalCallContext);
         final List<InvoiceModelDao> invoices = invoiceDao.getInvoicesByAccount(account.getId(), internalCallContext);
         Assert.assertEquals(invoices.size(), 1);
         Assert.assertEquals(invoices.get(0).getInvoiceItems().size(), 1);
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
index 8ddc345..466d466 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
@@ -90,7 +90,11 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
         final PlanPhaseSpecifier spec = existingEvent.getPlanPhaseSpecifier();
         final PlanPhase currentPhase = existingEvent.getPlanPhaseName() != null ? catalog.findPhase(existingEvent.getPlanPhaseName(), effectiveDate, subscription.getAlignStartDate()) : null;
 
-        final ApiEventBuilder apiBuilder = currentPhase != null ? new ApiEventBuilder()
+        if (spec == null || currentPhase == null) {
+            // Ignore cancellations - we assume that transferred subscriptions should always be active
+            return null;
+        }
+        final ApiEventBuilder apiBuilder = new ApiEventBuilder()
                 .setSubscriptionId(subscription.getId())
                 .setEventPlan(currentPhase.getPlan().getName())
                 .setEventPlanPhase(currentPhase.getName())
@@ -100,7 +104,7 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
                 .setEffectiveDate(effectiveDate)
                 .setRequestedDate(effectiveDate)
                 .setUserToken(context.getUserToken())
-                .setFromDisk(true) : null;
+                .setFromDisk(true);
 
         switch (existingEvent.getSubscriptionTransitionType()) {
             case TRANSFER:
@@ -121,15 +125,17 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
                 break;
 
             // Ignore these events except if it's the first event for the new subscription
-            case CANCEL:
-            case UNCANCEL:
             case MIGRATE_BILLING:
                 if (firstEvent) {
                     newEvent = new ApiEventTransfer(apiBuilder);
                 }
                 break;
+            case CANCEL:
+            case UNCANCEL:
+                break;
+
             default:
-                throw new EntitlementError(String.format("Unepxected transitionType %s", existingEvent.getSubscriptionTransitionType()));
+                throw new EntitlementError(String.format("Unexpected transitionType %s", existingEvent.getSubscriptionTransitionType()));
         }
         return newEvent;
     }
@@ -187,8 +193,8 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
     public SubscriptionBundle transferBundle(final UUID sourceAccountId, final UUID destAccountId,
                                              final String bundleKey, final DateTime transferDate, final boolean transferAddOn,
                                              final boolean cancelImmediately, final CallContext context) throws EntitlementTransferApiException {
-        // Source or destination account?
-        final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(sourceAccountId, context);
+        final InternalCallContext fromInternalCallContext = internalCallContextFactory.createInternalCallContext(sourceAccountId, context);
+        final InternalCallContext toInternalCallContext = internalCallContextFactory.createInternalCallContext(destAccountId, context);
 
         try {
             final DateTime effectiveTransferDate = transferDate == null ? clock.getUTCNow() : transferDate;
@@ -198,7 +204,7 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
                 throw new EntitlementTransferApiException(ErrorCode.ENT_TRANSFER_INVALID_EFF_DATE, effectiveTransferDate);
             }
 
-            final SubscriptionBundle bundle = dao.getSubscriptionBundleFromAccountAndKey(sourceAccountId, bundleKey, internalCallContext);
+            final SubscriptionBundle bundle = dao.getSubscriptionBundleFromAccountAndKey(sourceAccountId, bundleKey, fromInternalCallContext);
             if (bundle == null) {
                 throw new EntitlementTransferApiException(ErrorCode.ENT_CREATE_NO_BUNDLE, bundleKey);
             }
@@ -214,7 +220,7 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
             DateTime bundleStartdate = null;
 
             for (final SubscriptionTimeline cur : bundleTimeline.getSubscriptions()) {
-                final SubscriptionData oldSubscription = (SubscriptionData) dao.getSubscriptionFromId(subscriptionFactory, cur.getId(), internalCallContext);
+                final SubscriptionData oldSubscription = (SubscriptionData) dao.getSubscriptionFromId(subscriptionFactory, cur.getId(), fromInternalCallContext);
                 final List<ExistingEvent> existingEvents = cur.getExistingEvents();
                 final ProductCategory productCategory = existingEvents.get(0).getPlanPhaseSpecifier().getProductCategory();
                 if (productCategory == ProductCategory.ADD_ON) {
@@ -263,7 +269,7 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
             BundleMigrationData bundleMigrationData = new BundleMigrationData(subscriptionBundleData, subscriptionMigrationDataList);
 
             // Atomically cancel all subscription on old account and create new bundle, subscriptions, events for new account
-            dao.transfer(sourceAccountId, destAccountId, bundleMigrationData, transferCancelDataList, internalCallContext);
+            dao.transfer(sourceAccountId, destAccountId, bundleMigrationData, transferCancelDataList, fromInternalCallContext, toInternalCallContext);
 
             return bundle;
         } catch (EntitlementRepairException e) {
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 d24d121..4831fdc 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,96 @@ 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 is 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;
+        String curPlan = null;
+        String curPhase = null;
+        String curPriceList = null;
+        for (EntitlementEvent cur : changeEvents) {
+            switch (cur.getType()) {
+                case API_USER:
+                    final ApiEvent apiEvent = (ApiEvent) cur;
+                    curPlan = apiEvent.getEventPlan();
+                    curPhase = apiEvent.getEventPlanPhase();
+                    curPriceList = apiEvent.getPriceList();
+                    break;
+
+                case PHASE:
+                    final PhaseEvent phaseEvent = (PhaseEvent) cur;
+                    curPhase = 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;
+            }
+            prevPlan = curPlan;
+            prevPhase = curPhase;
+            prevPriceList = curPriceList;
+        }
+
+        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 +631,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 +649,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) {
@@ -711,7 +820,7 @@ public class DefaultEntitlementDao implements EntitlementDao {
 
     @Override
     public void transfer(final UUID srcAccountId, final UUID destAccountId, final BundleMigrationData bundleTransferData,
-                         final List<TransferCancelData> transferCancelData, final InternalCallContext context) {
+                         final List<TransferCancelData> transferCancelData, final InternalCallContext fromContext, final InternalCallContext toContext) {
 
         transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
             @Override
@@ -720,10 +829,10 @@ public class DefaultEntitlementDao implements EntitlementDao {
 
                 // Cancel the subscriptions for the old bundle
                 for (final TransferCancelData cancel : transferCancelData) {
-                    cancelSubscriptionFromTransaction(cancel.getSubscription(), cancel.getCancelEvent(), entitySqlDaoWrapperFactory, context, 0);
+                    cancelSubscriptionFromTransaction(cancel.getSubscription(), cancel.getCancelEvent(), entitySqlDaoWrapperFactory, fromContext, 0);
                 }
 
-                migrateBundleDataFromTransaction(bundleTransferData, transactional, entitySqlDaoWrapperFactory, context);
+                migrateBundleDataFromTransaction(bundleTransferData, transactional, entitySqlDaoWrapperFactory, toContext);
                 return null;
             }
         });
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementDao.java
index a9b900d..1c17929 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementDao.java
@@ -85,7 +85,7 @@ public interface EntitlementDao {
 
     public void migrate(UUID accountId, AccountMigrationData data, InternalCallContext context);
 
-    public void transfer(UUID srcAccountId, UUID destAccountId, BundleMigrationData data, List<TransferCancelData> transferCancelData, InternalCallContext context);
+    public void transfer(UUID srcAccountId, UUID destAccountId, BundleMigrationData data, List<TransferCancelData> transferCancelData, InternalCallContext fromContext, InternalCallContext toContext);
 
     // Repair
     public void repair(UUID accountId, UUID bundleId, List<SubscriptionDataRepair> inRepair, InternalCallContext context);
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/RepairEntitlementDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/RepairEntitlementDao.java
index 032e13c..b4d3899 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/RepairEntitlementDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/RepairEntitlementDao.java
@@ -290,7 +290,8 @@ public class RepairEntitlementDao implements EntitlementDao, RepairEntitlementLi
 
     @Override
     public void transfer(final UUID srcAccountId, final UUID destAccountId, final BundleMigrationData data,
-                         final List<TransferCancelData> transferCancelData, final InternalCallContext context) {
+                         final List<TransferCancelData> transferCancelData, final InternalCallContext fromContext,
+                         final InternalCallContext toContext) {
         throw new EntitlementError(NOT_IMPLEMENTED);
     }
 
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/TestDefaultEntitlementTransferApi.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestDefaultEntitlementTransferApi.java
index ced19b9..2f21fe2 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestDefaultEntitlementTransferApi.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestDefaultEntitlementTransferApi.java
@@ -70,6 +70,39 @@ public class TestDefaultEntitlementTransferApi extends EntitlementTestSuite {
     }
 
     @Test(groups = "fast")
+    public void testEventsForCancelledSubscriptionBeforeTransfer() throws Exception {
+        final DateTime subscriptionStartTime = clock.getUTCNow();
+        final DateTime subscriptionCancelTime = subscriptionStartTime.plusDays(1);
+        final ImmutableList<ExistingEvent> existingEvents = ImmutableList.<ExistingEvent>of(createEvent(subscriptionStartTime, SubscriptionTransitionType.CREATE),
+                                                                                            createEvent(subscriptionCancelTime, SubscriptionTransitionType.CANCEL));
+        final SubscriptionBuilder subscriptionBuilder = new SubscriptionBuilder();
+        final SubscriptionData subscription = new SubscriptionData(subscriptionBuilder);
+
+        final DateTime transferDate = subscriptionStartTime.plusDays(10);
+        final List<EntitlementEvent> events = transferApi.toEvents(existingEvents, subscription, transferDate, callContext);
+
+        Assert.assertEquals(events.size(), 0);
+    }
+
+    @Test(groups = "fast")
+    public void testEventsForCancelledSubscriptionAfterTransfer() throws Exception {
+        final DateTime subscriptionStartTime = clock.getUTCNow();
+        final DateTime subscriptionCancelTime = subscriptionStartTime.plusDays(1);
+        final ImmutableList<ExistingEvent> existingEvents = ImmutableList.<ExistingEvent>of(createEvent(subscriptionStartTime, SubscriptionTransitionType.CREATE),
+                                                                                            createEvent(subscriptionCancelTime, SubscriptionTransitionType.CANCEL));
+        final SubscriptionBuilder subscriptionBuilder = new SubscriptionBuilder();
+        final SubscriptionData subscription = new SubscriptionData(subscriptionBuilder);
+
+        final DateTime transferDate = subscriptionStartTime.plusHours(1);
+        final List<EntitlementEvent> events = transferApi.toEvents(existingEvents, subscription, transferDate, callContext);
+
+        Assert.assertEquals(events.size(), 1);
+        Assert.assertEquals(events.get(0).getType(), EventType.API_USER);
+        Assert.assertEquals(events.get(0).getEffectiveDate(), transferDate);
+        Assert.assertEquals(((ApiEventTransfer) events.get(0)).getEventType(), ApiEventType.TRANSFER);
+    }
+
+    @Test(groups = "fast")
     public void testEventsAfterTransferForMigratedBundle1() throws Exception {
         // MIGRATE_ENTITLEMENT then MIGRATE_BILLING (both in the past)
         final DateTime transferDate = clock.getUTCNow();
@@ -134,6 +167,42 @@ public class TestDefaultEntitlementTransferApi extends EntitlementTestSuite {
         return transferApi.toEvents(existingEvents, subscription, transferDate, callContext);
     }
 
+    private ExistingEvent createEvent(final DateTime eventEffectiveDate, final SubscriptionTransitionType subscriptionTransitionType) {
+        return new ExistingEvent() {
+            @Override
+            public DateTime getEffectiveDate() {
+                return eventEffectiveDate;
+            }
+
+            @Override
+            public String getPlanPhaseName() {
+                return SubscriptionTransitionType.CANCEL.equals(subscriptionTransitionType) ? null : "BicycleTrialEvergreen1USD-trial";
+            }
+
+            @Override
+            public UUID getEventId() {
+                return UUID.randomUUID();
+            }
+
+            @Override
+            public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+                return SubscriptionTransitionType.CANCEL.equals(subscriptionTransitionType) ? null :
+                       new PlanPhaseSpecifier("BicycleTrialEvergreen1USD", ProductCategory.BASE, BillingPeriod.NO_BILLING_PERIOD,
+                                              PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.FIXEDTERM);
+            }
+
+            @Override
+            public DateTime getRequestedDate() {
+                return getEffectiveDate();
+            }
+
+            @Override
+            public SubscriptionTransitionType getSubscriptionTransitionType() {
+                return subscriptionTransitionType;
+            }
+        };
+    }
+
     private ImmutableList<ExistingEvent> createMigrateEvents(final DateTime migrateEntitlementEventEffectiveDate, final DateTime migrateBillingEventEffectiveDate) {
         final ExistingEvent migrateEntitlementEvent = new ExistingEvent() {
             @Override
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);
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoMemory.java b/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoMemory.java
index 453d64d..b73c1e3 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoMemory.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoMemory.java
@@ -432,6 +432,7 @@ public class MockEntitlementDaoMemory implements EntitlementDao {
 
     @Override
     public void transfer(final UUID srcAccountId, final UUID destAccountId, final BundleMigrationData data,
-                         final List<TransferCancelData> transferCancelData, final InternalCallContext context) {
+                         final List<TransferCancelData> transferCancelData, final InternalCallContext fromContext,
+                         final InternalCallContext toContext) {
     }
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java b/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
index e613e9d..2e48838 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/migration/DefaultInvoiceMigrationApi.java
@@ -19,6 +19,7 @@ package com.ning.billing.invoice.api.migration;
 import java.math.BigDecimal;
 import java.util.UUID;
 
+import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -38,6 +39,7 @@ import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.svcapi.account.AccountInternalApi;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 
 public class DefaultInvoiceMigrationApi implements InvoiceMigrationApi {
@@ -74,7 +76,7 @@ public class DefaultInvoiceMigrationApi implements InvoiceMigrationApi {
                                                                                  MigrationPlan.MIGRATION_PLAN_NAME, MigrationPlan.MIGRATION_PLAN_PHASE_NAME,
                                                                                  targetDate, null, balance, null, currency, null);
         dao.createInvoice(migrationInvoice, ImmutableList.<InvoiceItemModelDao>of(migrationInvoiceItem),
-                          ImmutableList.<InvoicePaymentModelDao>of(), true, internalCallContextFactory.createInternalCallContext(accountId, context));
+                          ImmutableList.<InvoicePaymentModelDao>of(), true, ImmutableMap.<UUID, DateTime>of(), internalCallContextFactory.createInternalCallContext(accountId, context));
 
         return migrationInvoice.getId();
     }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
index 142ac74..fe64a71 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
@@ -25,6 +25,7 @@ import java.util.UUID;
 
 import javax.annotation.Nullable;
 
+import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 import org.skife.jdbi.v2.IDBI;
 import org.slf4j.Logger;
@@ -177,7 +178,8 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
 
     @Override
     public void createInvoice(final InvoiceModelDao invoice, final List<InvoiceItemModelDao> invoiceItems,
-                              final List<InvoicePaymentModelDao> invoicePayments, final boolean isRealInvoice, final InternalCallContext context) {
+                              final List<InvoicePaymentModelDao> invoicePayments, final boolean isRealInvoice, final Map<UUID, DateTime> callbackDateTimePerSubscriptions,
+                              final InternalCallContext context) {
         transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
             @Override
             public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
@@ -197,14 +199,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                         transInvoiceItemSqlDao.create(invoiceItemModelDao, context);
                     }
 
-                    // Add entries in the notification queue for recurring items
-                    final List<InvoiceItemModelDao> recurringInvoiceItems = ImmutableList.<InvoiceItemModelDao>copyOf(Collections2.filter(invoiceItems, new Predicate<InvoiceItemModelDao>() {
-                        @Override
-                        public boolean apply(@Nullable final InvoiceItemModelDao item) {
-                            return item.getType() == InvoiceItemType.RECURRING;
-                        }
-                    }));
-                    notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), recurringInvoiceItems);
+                    notifyOfFutureBillingEvents(entitySqlDaoWrapperFactory, invoice.getAccountId(), callbackDateTimePerSubscriptions);
 
                     // Create associated payments
                     final InvoicePaymentSqlDao invoicePaymentSqlDao = entitySqlDaoWrapperFactory.become(InvoicePaymentSqlDao.class);
@@ -944,20 +939,12 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
         invoice.addPayments(invoicePayments);
     }
 
-    private void notifyOfFutureBillingEvents(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final UUID accountId, final List<InvoiceItemModelDao> invoiceItems) {
+    private void notifyOfFutureBillingEvents(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final UUID accountId, final Map<UUID, DateTime> callbackDateTimePerSubscriptions) {
 
-        for (final InvoiceItemModelDao item : invoiceItems) {
-            if (item.getType() == InvoiceItemType.RECURRING) {
-                if ((item.getEndDate() != null) &&
-                    (item.getAmount() == null ||
-                     item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
-                    //
-                    // We insert a future notification for each recurring subscription at the end of the service period  = new CTD of the subscription
-                    //
-                    nextBillingDatePoster.insertNextBillingNotification(entitySqlDaoWrapperFactory, accountId, item.getSubscriptionId(),
-                                                                        item.getEndDate().toDateTimeAtCurrentTime());
-                }
-            }
+
+        for (UUID subscriptionId : callbackDateTimePerSubscriptions.keySet()) {
+            final DateTime callbackDateTimeUTC = callbackDateTimePerSubscriptions.get(subscriptionId);
+            nextBillingDatePoster.insertNextBillingNotification(entitySqlDaoWrapperFactory, accountId, subscriptionId, callbackDateTimeUTC);
         }
     }
 
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
index 1d51614..dbaa474 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
@@ -23,6 +23,7 @@ import java.util.UUID;
 
 import javax.annotation.Nullable;
 
+import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 
 import com.ning.billing.catalog.api.Currency;
@@ -33,7 +34,7 @@ import com.ning.billing.util.callcontext.InternalTenantContext;
 public interface InvoiceDao {
 
     void createInvoice(InvoiceModelDao invoice, List<InvoiceItemModelDao> invoiceItems,
-                       List<InvoicePaymentModelDao> invoicePayments, boolean isRealInvoice, InternalCallContext context);
+                       List<InvoicePaymentModelDao> invoicePayments, boolean isRealInvoice, final Map<UUID, DateTime> callbackDateTimePerSubscriptions, InternalCallContext context);
 
     InvoiceModelDao getById(UUID id, InternalTenantContext context);
 
diff --git a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
index b7aefc8..7873fe6 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/InvoiceDispatcher.java
@@ -16,6 +16,7 @@
 
 package com.ning.billing.invoice;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -26,6 +27,7 @@ import java.util.UUID;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
+import org.joda.time.LocalTime;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -38,6 +40,7 @@ import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.invoice.api.InvoiceNotifier;
 import com.ning.billing.invoice.api.InvoicePayment;
 import com.ning.billing.invoice.api.user.DefaultInvoiceCreationEvent;
@@ -67,6 +70,7 @@ import com.ning.billing.util.svcapi.junction.BillingInternalApi;
 import com.ning.billing.util.svcsapi.bus.InternalBus;
 import com.ning.billing.util.svcsapi.bus.InternalBus.EventBusException;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
@@ -208,7 +212,9 @@ public class InvoiceDispatcher {
                                                                                                                                                              return new InvoicePaymentModelDao(input);
                                                                                                                                                          }
                                                                                                                                                      }));
-                    invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, isRealInvoiceWithItems, context);
+
+                    final Map<UUID, DateTime> callbackDateTimePerSubscriptions = createNextFutureNotificationDate(invoiceItemModelDaos, account.getTimeZone());
+                    invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, isRealInvoiceWithItems, callbackDateTimePerSubscriptions, context);
 
                     final List<InvoiceItem> fixedPriceInvoiceItems = invoice.getInvoiceItems(FixedPriceInvoiceItem.class);
                     final List<InvoiceItem> recurringInvoiceItems = invoice.getInvoiceItems(RecurringInvoiceItem.class);
@@ -239,6 +245,37 @@ public class InvoiceDispatcher {
         }
     }
 
+
+    @VisibleForTesting
+    Map<UUID, DateTime> createNextFutureNotificationDate(final List<InvoiceItemModelDao> invoiceItems, final DateTimeZone accountTimeZone) {
+        final Map<UUID, DateTime> result = new HashMap<UUID, DateTime>();
+
+        // For each subscription that has a positive (amount) recurring item, create the date
+        // at which we should be called back for next invoice.
+        //
+        for (final InvoiceItemModelDao item : invoiceItems) {
+            if (item.getType() == InvoiceItemType.RECURRING) {
+                if ((item.getEndDate() != null) &&
+                    (item.getAmount() == null ||
+                     item.getAmount().compareTo(BigDecimal.ZERO) >= 0)) {
+
+                    //
+                    // Since we create the targetDate for next invoice using that date (on the way back), we need to make sure
+                    // that this datetime once transformed into a LocalDate points to the correct day.
+                    // e.g If accountTimeZone is -8 and we want to invoice on the 16, with a toDateTimeAtCurrentTime = 00:00:23,
+                    // we will generate a datetime that is 16T08:00:23 => LocalDate in that timeZone stays on the 16.
+                    //
+                    final int deltaMs = accountTimeZone.getOffset(clock.getUTCNow());
+                    final int negativeDeltaMs = -1 * deltaMs;
+
+                    final LocalTime localTime = clock.getUTCNow().toLocalTime();
+                    result.put(item.getSubscriptionId(), item.getEndDate().toDateTime(localTime, DateTimeZone.UTC).plusMillis(negativeDeltaMs));
+                }
+            }
+        }
+        return result;
+    }
+
     private void setChargedThroughDates(final BillCycleDay billCycleDay,
                                         final DateTimeZone accountTimeZone,
                                         final Collection<InvoiceItem> fixedPriceItems,
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/migration/InvoiceApiTestBase.java b/invoice/src/test/java/com/ning/billing/invoice/api/migration/InvoiceApiTestBase.java
index 42518d7..d4ee2e1 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/api/migration/InvoiceApiTestBase.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/migration/InvoiceApiTestBase.java
@@ -21,6 +21,7 @@ import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.AfterSuite;
@@ -167,6 +168,8 @@ public abstract class InvoiceApiTestBase extends InvoicingTestBase {
         Mockito.when(account.getId()).thenReturn(accountId);
         Mockito.when(account.isNotifiedForInvoices()).thenReturn(true);
         Mockito.when(account.getBillCycleDay()).thenReturn(new MockBillCycleDay(31));
+        // The timezone is required to compute the date of the next invoice notification
+        Mockito.when(account.getTimeZone()).thenReturn(DateTimeZone.UTC);
 
         return account;
     }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
index ad8c7e0..39e6e1d 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/InvoiceDaoTestBase.java
@@ -21,6 +21,7 @@ import java.net.URL;
 import java.util.List;
 import java.util.UUID;
 
+import org.joda.time.DateTime;
 import org.skife.jdbi.v2.IDBI;
 import org.testng.Assert;
 import org.testng.annotations.AfterClass;
@@ -49,6 +50,7 @@ import com.ning.billing.util.svcsapi.bus.InternalBus;
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
@@ -146,7 +148,8 @@ public class InvoiceDaoTestBase extends InvoicingTestBase {
                                                                                                                                              }
                                                                                                                                          }));
 
-        invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, isRealInvoiceWithItems, internalCallContext);
+        // The test does not use the invoice callback notifier hence the empty map
+        invoiceDao.createInvoice(invoiceModelDao, invoiceItemModelDaos, invoicePaymentModelDaos, isRealInvoiceWithItems, ImmutableMap.<UUID, DateTime>of(), internalCallContext);
     }
 
     protected void verifyInvoice(final UUID invoiceId, final double balance, final double cbaAmount) throws InvoiceApiException {
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
index cdcc683..6861d13 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
@@ -26,6 +26,7 @@ import java.util.UUID;
 
 import javax.annotation.Nullable;
 
+import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 
 import com.ning.billing.catalog.api.Currency;
@@ -52,7 +53,7 @@ public class MockInvoiceDao implements InvoiceDao {
 
     @Override
     public void createInvoice(final InvoiceModelDao invoice, final List<InvoiceItemModelDao> invoiceItems,
-                              final List<InvoicePaymentModelDao> invoicePayments, final boolean isRealInvoice, final InternalCallContext context) {
+                              final List<InvoicePaymentModelDao> invoicePayments, final boolean isRealInvoice, final Map<UUID, DateTime> callbackDateTimePerSubscriptions, final InternalCallContext context) {
         synchronized (monitor) {
             invoices.put(invoice.getId(), invoice);
             for (final InvoiceItemModelDao invoiceItemModelDao : invoiceItems) {
diff --git a/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
index 95341d8..23559e2 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/TestInvoiceDispatcher.java
@@ -17,10 +17,13 @@
 package com.ning.billing.invoice;
 
 import java.math.BigDecimal;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
 import org.mockito.Mockito;
 import org.slf4j.Logger;
@@ -49,6 +52,7 @@ import com.ning.billing.invoice.api.InvoiceItem;
 import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.invoice.api.InvoiceNotifier;
 import com.ning.billing.invoice.dao.InvoiceDao;
+import com.ning.billing.invoice.dao.InvoiceItemModelDao;
 import com.ning.billing.invoice.dao.InvoiceModelDao;
 import com.ning.billing.invoice.generator.InvoiceGenerator;
 import com.ning.billing.invoice.notification.NextBillingDateNotifier;
@@ -59,7 +63,7 @@ import com.ning.billing.util.bus.DefaultBusService;
 import com.ning.billing.util.callcontext.InternalCallContext;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.util.callcontext.InternalTenantContext;
-import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
 import com.ning.billing.util.globallocker.GlobalLocker;
 import com.ning.billing.util.svcapi.account.AccountInternalApi;
 import com.ning.billing.util.svcapi.entitlement.EntitlementInternalApi;
@@ -97,7 +101,7 @@ public class TestInvoiceDispatcher extends InvoicingTestBase {
     private BillingInternalApi billingApi;
 
     @Inject
-    private Clock clock;
+    private ClockMock clock;
 
     @Inject
     private AccountInternalApi accountInternalApi;
@@ -118,7 +122,6 @@ public class TestInvoiceDispatcher extends InvoicingTestBase {
 
         busService.getBus().start();
 
-        //accountInternalApi = Mockito.mock(AccountInternalApi.class);
         account = Mockito.mock(Account.class);
 
         final UUID accountId = UUID.randomUUID();
@@ -129,6 +132,8 @@ public class TestInvoiceDispatcher extends InvoicingTestBase {
         Mockito.when(account.getId()).thenReturn(accountId);
         Mockito.when(account.isNotifiedForInvoices()).thenReturn(true);
         Mockito.when(account.getBillCycleDay()).thenReturn(new MockBillCycleDay(30));
+        // The timezone is required to compute the date of the next invoice notification
+        Mockito.when(account.getTimeZone()).thenReturn(DateTimeZone.UTC);
 
         subscription = Mockito.mock(Subscription.class);
         final UUID subscriptionId = UUID.randomUUID();
@@ -263,5 +268,39 @@ public class TestInvoiceDispatcher extends InvoicingTestBase {
         }
     }
 
+    @Test(groups= "slow")
+    public void testCreateNextFutureNotificationDate() throws Exception {
+
+        final LocalDate startDate = new LocalDate("2012-10-26");
+        final LocalDate endDate = new LocalDate("2012-11-26");
+
+        clock.setTime(new DateTime(2012, 10, 26, 1, 12, 23, DateTimeZone.UTC));
+        final InvoiceItemModelDao item = new InvoiceItemModelDao(UUID.randomUUID(), clock.getUTCNow(), InvoiceItemType.RECURRING, UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(),
+                                                           "planName", "phaseName", startDate, endDate, new BigDecimal("23.9"), new BigDecimal("23.9"), Currency.EUR, null);
+
+        final InvoiceNotifier invoiceNotifier = new NullInvoiceNotifier();
+        final InvoiceDispatcher dispatcher = new InvoiceDispatcher(generator, accountInternalApi, billingApi, entitlementInternalApi, invoiceDao,
+                                                                   invoiceNotifier, locker, busService.getBus(),
+                                                                   clock);
+
+        final DateTime expectedBefore = clock.getUTCNow();
+        final Map<UUID, DateTime> result = dispatcher.createNextFutureNotificationDate(Collections.singletonList(item), DateTimeZone.forID("Pacific/Pitcairn"));
+        final DateTime expectedAfter =  clock.getUTCNow();
+
+        Assert.assertEquals(result.size(), 1);
+
+        final DateTime receivedDate = result.get(item.getSubscriptionId());
+
+        final LocalDate receivedTargetDate = new LocalDate(receivedDate, DateTimeZone.forID("Pacific/Pitcairn"));
+        Assert.assertEquals(receivedTargetDate, endDate);
+
+
+
+        Assert.assertTrue(receivedDate.compareTo(new DateTime(2012, 11, 26, 9 /* 1 + 8 for Pitcairn */ , 12, 23, DateTimeZone.UTC)) >= 0);
+        Assert.assertTrue(receivedDate.compareTo(new DateTime(2012, 11, 26, 9, 13, 0, DateTimeZone.UTC)) <= 0);
+
+    }
+
+
     //MDW add a test to cover when the account auto-invoice-off tag is present
 }