killbill-memoizeit
Changes
entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java 26(+16 -10)
entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/DefaultEntitlementDao.java 123(+116 -7)
entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/RepairEntitlementDao.java 3(+2 -1)
entitlement/src/test/java/com/ning/billing/entitlement/api/transfer/TestDefaultEntitlementTransferApi.java 69(+69 -0)
entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoMemory.java 3(+2 -1)
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
}