killbill-memoizeit

subscription: entitlement: cancel add-ons during transfer Add-ons

11/18/2013 2:08:22 PM

Details

diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
index ac15df5..6a15797 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestBundleTransfer.java
@@ -18,22 +18,30 @@ package com.ning.billing.beatrix.integration;
 
 import java.math.BigDecimal;
 import java.util.List;
+import java.util.UUID;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import com.ning.billing.account.api.Account;
 import com.ning.billing.api.TestApiListener.NextEvent;
 import com.ning.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
+import com.ning.billing.beatrix.util.PaymentChecker.ExpectedPaymentCheck;
 import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.catalog.api.PlanPhaseSpecifier;
 import com.ning.billing.catalog.api.PriceListSet;
 import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.DefaultEntitlement;
+import com.ning.billing.entitlement.api.Entitlement;
+import com.ning.billing.entitlement.api.Subscription;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceItem;
 import com.ning.billing.invoice.api.InvoiceItemType;
+import com.ning.billing.payment.api.PaymentStatus;
 
 import com.google.common.collect.ImmutableList;
 
@@ -212,4 +220,93 @@ public class TestBundleTransfer extends TestIntegrationBase {
                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 3), new LocalDate(2012, 5, 15), InvoiceItemType.RECURRING, new BigDecimal("99.98")));
         invoiceChecker.checkInvoice(invoices.get(0).getId(), callContext, toBeChecked);
     }
+
+    @Test(groups = "slow", description = "Test entitlement-level transfer with add-on")
+    public void testBundleTransferWithAddOn() throws Exception {
+        final LocalDate startDate = new LocalDate(2012, 4, 1);
+        clock.setDay(startDate);
+
+        // Share the BCD on both accounts for simplicity
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+        final Account newAccount = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+        final String bpProductName = "Shotgun";
+        final String aoProductName = "Telescopic-Scope";
+
+        // Create the base plan
+        final String bundleExternalKey = UUID.randomUUID().toString();
+        final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), bundleExternalKey, bpProductName, ProductCategory.BASE, term,
+                                                                                            NextEvent.CREATE, NextEvent.INVOICE);
+        subscriptionChecker.checkSubscriptionCreated(bpEntitlement.getId(), internalCallContext);
+        final Invoice firstInvoice = invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), null, InvoiceItemType.FIXED, new BigDecimal("0")));
+
+        // Create the add-on
+        final DefaultEntitlement aoEntitlement = addAOEntitlementAndCheckForCompletion(bpEntitlement.getBundleId(), aoProductName, ProductCategory.ADD_ON, term,
+                                                                                       NextEvent.CREATE, NextEvent.INVOICE, NextEvent.PAYMENT);
+        final Invoice secondInvoice = invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2012, 4, 1), new LocalDate(2012, 5, 1), InvoiceItemType.RECURRING, new BigDecimal("399.95")));
+        paymentChecker.checkPayment(account.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 4, 1), new BigDecimal("399.95"), PaymentStatus.SUCCESS, secondInvoice.getId(), Currency.USD));
+
+        // Move past the phase for simplicity
+        busHandler.pushExpectedEvents(NextEvent.PHASE, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT);
+        clock.addDays(30);
+        assertListenerStatus();
+        final Invoice thirdInvoice = invoiceChecker.checkInvoice(account.getId(), 3, callContext,
+                                                                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("999.95")),
+                                                                 new ExpectedInvoiceItemCheck(new LocalDate(2012, 5, 1), new LocalDate(2012, 6, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        paymentChecker.checkPayment(account.getId(), 2, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 5, 1), new BigDecimal("1249.90"), PaymentStatus.SUCCESS, thirdInvoice.getId(), Currency.USD));
+
+        // Align the transfer on the BCD to make pro-rations easier
+        busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.PAYMENT);
+        // Move a bit the time to make sure notifications kick in
+        clock.setTime(new DateTime(2012, 6, 1, 1, 0, DateTimeZone.UTC));
+        assertListenerStatus();
+        final Invoice fourthInvoice = invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+                                                                  new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("999.95")),
+                                                                  new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        paymentChecker.checkPayment(account.getId(), 3, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 6, 1), new BigDecimal("1249.90"), PaymentStatus.SUCCESS, fourthInvoice.getId(), Currency.USD));
+
+        final DateTime now = clock.getUTCNow();
+        final LocalDate transferDay = now.toLocalDate();
+
+        busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.BLOCK, NextEvent.TRANSFER, NextEvent.TRANSFER, NextEvent.INVOICE_ADJUSTMENT, NextEvent.INVOICE, NextEvent.PAYMENT);
+        final UUID newBundleId = entitlementApi.transferEntitlements(account.getId(), newAccount.getId(), bundleExternalKey, transferDay, callContext);
+        assertListenerStatus();
+
+        // Check the last invoice on the old account
+        invoiceChecker.checkInvoice(account.getId(), 4, callContext,
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("999.95")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-999.95")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-249.95")),
+                                    new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 6, 1), InvoiceItemType.CBA_ADJ, new BigDecimal("1249.90")));
+
+        // Check the first invoice and payment on the new account
+        final Invoice firstInvoiceNewAccount = invoiceChecker.checkInvoice(newAccount.getId(), 1, callContext,
+                                                                           new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("999.95")),
+                                                                           new ExpectedInvoiceItemCheck(new LocalDate(2012, 6, 1), new LocalDate(2012, 7, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
+        paymentChecker.checkPayment(newAccount.getId(), 1, callContext, new ExpectedPaymentCheck(new LocalDate(2012, 6, 1), new BigDecimal("1249.90"), PaymentStatus.SUCCESS, firstInvoiceNewAccount.getId(), Currency.USD));
+
+        // Check entitlements and subscriptions on the old account
+        final List<Entitlement> oldEntitlements = entitlementApi.getAllEntitlementsForBundle(bpEntitlement.getBundleId(), callContext);
+        Assert.assertEquals(oldEntitlements.size(), 2);
+        for (final Entitlement entitlement : oldEntitlements) {
+            final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(entitlement.getId(), callContext);
+            Assert.assertEquals(subscription.getEffectiveStartDate(), startDate);
+            Assert.assertEquals(subscription.getEffectiveEndDate(), transferDay);
+            Assert.assertEquals(subscription.getBillingStartDate(), startDate);
+            Assert.assertEquals(subscription.getBillingEndDate(), transferDay);
+        }
+
+        // Check entitlements and subscriptions on the new account
+        final List<Entitlement> newEntitlements = entitlementApi.getAllEntitlementsForBundle(newBundleId, callContext);
+        Assert.assertEquals(newEntitlements.size(), 2);
+        for (final Entitlement entitlement : newEntitlements) {
+            final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(entitlement.getId(), callContext);
+            Assert.assertEquals(subscription.getEffectiveStartDate(), transferDay);
+            Assert.assertNull(subscription.getEffectiveEndDate());
+            Assert.assertEquals(subscription.getBillingStartDate(), transferDay);
+            Assert.assertNull(subscription.getBillingEndDate());
+        }
+    }
 }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEntitlementApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEntitlementApi.java
index 96e7ed0..98c7a8b 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEntitlementApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEntitlementApi.java
@@ -305,7 +305,6 @@ public class DefaultEntitlementApi implements EntitlementApi {
         }
     }
 
-
     @Override
     public UUID transferEntitlements(final UUID sourceAccountId, final UUID destAccountId, final String externalKey, final LocalDate effectiveDate, final CallContext context) throws EntitlementApiException {
         return transferEntitlementsOverrideBillingPolicy(sourceAccountId, destAccountId, externalKey, effectiveDate, BillingActionPolicy.IMMEDIATE, context);
@@ -336,8 +335,12 @@ public class DefaultEntitlementApi implements EntitlementApi {
             final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(effectiveDate, baseSubscription.getStartDate(), contextWithValidAccountRecordId);
             final SubscriptionBaseBundle newBundle = subscriptionTransferApi.transferBundle(sourceAccountId, destAccountId, externalKey, requestedDate, true, cancelImm, context);
 
-            final BlockingState newBlockingState = new DefaultBlockingState(bundle.getId(), BlockingStateType.SUBSCRIPTION_BUNDLE, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, requestedDate);
-            entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(newBlockingState, contextWithValidAccountRecordId);
+            // Block all associated subscriptions - TODO Do we want to block the bundle as well (this will add an extra STOP_ENTITLEMENT event in the bundle timeline stream)?
+            // Note that there is no un-transfer at the moment, so we effectively add a blocking state on disk for all subscriptions
+            for (final SubscriptionBase subscriptionBase : subscriptionInternalApi.getSubscriptionsForBundle(bundle.getId(), contextWithValidAccountRecordId)) {
+                final BlockingState blockingState = new DefaultBlockingState(subscriptionBase.getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, requestedDate);
+                entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(blockingState, contextWithValidAccountRecordId);
+            }
 
             return newBundle.getId();
         } catch (SubscriptionBaseTransferApiException e) {
diff --git a/subscription/src/main/java/com/ning/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java b/subscription/src/main/java/com/ning/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
index 7e62ee8..6c7fcc0 100644
--- a/subscription/src/main/java/com/ning/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
+++ b/subscription/src/main/java/com/ning/billing/subscription/api/transfer/DefaultSubscriptionBaseTransferApi.java
@@ -23,12 +23,14 @@ import java.util.UUID;
 import org.joda.time.DateTime;
 
 import com.ning.billing.ErrorCode;
+import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.catalog.api.Catalog;
 import com.ning.billing.catalog.api.CatalogApiException;
 import com.ning.billing.catalog.api.CatalogService;
 import com.ning.billing.catalog.api.PlanPhase;
 import com.ning.billing.catalog.api.PlanPhaseSpecifier;
 import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.clock.Clock;
 import com.ning.billing.entitlement.api.Entitlement.EntitlementState;
 import com.ning.billing.subscription.api.SubscriptionApiBase;
 import com.ning.billing.subscription.api.SubscriptionBaseApiService;
@@ -36,6 +38,9 @@ import com.ning.billing.subscription.api.migration.AccountMigrationData.BundleMi
 import com.ning.billing.subscription.api.migration.AccountMigrationData.SubscriptionMigrationData;
 import com.ning.billing.subscription.api.svcs.DefaultSubscriptionInternalApi;
 import com.ning.billing.subscription.api.timeline.BundleBaseTimeline;
+import com.ning.billing.subscription.api.timeline.SubscriptionBaseRepairException;
+import com.ning.billing.subscription.api.timeline.SubscriptionBaseTimeline;
+import com.ning.billing.subscription.api.timeline.SubscriptionBaseTimeline.ExistingEvent;
 import com.ning.billing.subscription.api.timeline.SubscriptionBaseTimelineApi;
 import com.ning.billing.subscription.api.user.DefaultSubscriptionBase;
 import com.ning.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
@@ -49,14 +54,8 @@ import com.ning.billing.subscription.events.user.ApiEventCancel;
 import com.ning.billing.subscription.events.user.ApiEventChange;
 import com.ning.billing.subscription.events.user.ApiEventTransfer;
 import com.ning.billing.subscription.exceptions.SubscriptionBaseError;
-import com.ning.billing.subscription.api.timeline.SubscriptionBaseRepairException;
-import com.ning.billing.subscription.api.timeline.SubscriptionBaseTimeline;
-import com.ning.billing.subscription.api.timeline.SubscriptionBaseTimeline.ExistingEvent;
-
 import com.ning.billing.util.callcontext.CallContext;
-import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
-import com.ning.billing.clock.Clock;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
@@ -226,29 +225,31 @@ public class DefaultSubscriptionBaseTransferApi extends SubscriptionApiBase impl
                 }
                 final List<ExistingEvent> existingEvents = cur.getExistingEvents();
                 final ProductCategory productCategory = existingEvents.get(0).getPlanPhaseSpecifier().getProductCategory();
-                if (productCategory == ProductCategory.ADD_ON) {
-                    if (!transferAddOn) {
-                        continue;
-                    }
-                } else {
 
-                    // If BP or STANDALONE subscription, create the cancelWithRequestedDate event on effectiveCancelDate
+                // For future add-on cancellations, don't add a cancellation on disk right away (mirror the behavior
+                // on base plan cancellations, even though we don't support un-transfer today)
+                if (productCategory != ProductCategory.ADD_ON || cancelImmediately) {
+                    // Create the cancelWithRequestedDate event on effectiveCancelDate
                     final DateTime effectiveCancelDate = !cancelImmediately && oldSubscription.getChargedThroughDate() != null &&
                                                          effectiveTransferDate.isBefore(oldSubscription.getChargedThroughDate()) ?
                                                          oldSubscription.getChargedThroughDate() : effectiveTransferDate;
 
                     final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
-                                                                                    .setSubscriptionId(cur.getId())
-                                                                                    .setActiveVersion(cur.getActiveVersion())
-                                                                                    .setProcessedDate(clock.getUTCNow())
-                                                                                    .setEffectiveDate(effectiveCancelDate)
-                                                                                    .setRequestedDate(effectiveTransferDate)
-                                                                                    .setFromDisk(true));
+                                                                                         .setSubscriptionId(cur.getId())
+                                                                                         .setActiveVersion(cur.getActiveVersion())
+                                                                                         .setProcessedDate(clock.getUTCNow())
+                                                                                         .setEffectiveDate(effectiveCancelDate)
+                                                                                         .setRequestedDate(effectiveTransferDate)
+                                                                                         .setFromDisk(true));
 
                     TransferCancelData cancelData = new TransferCancelData(oldSubscription, cancelEvent);
                     transferCancelDataList.add(cancelData);
                 }
 
+                if (productCategory == ProductCategory.ADD_ON && !transferAddOn) {
+                    continue;
+                }
+
                 // We Align with the original subscription
                 final DateTime subscriptionAlignStartDate = oldSubscription.getAlignStartDate();
                 if (bundleStartdate == null) {
@@ -257,12 +258,12 @@ public class DefaultSubscriptionBaseTransferApi extends SubscriptionApiBase impl
 
                 // Create the new subscription for the new bundle on the new account
                 final DefaultSubscriptionBase defaultSubscriptionBase = createSubscriptionForApiUse(new SubscriptionBuilder()
-                                                                                              .setId(UUID.randomUUID())
-                                                                                              .setBundleId(subscriptionBundleData.getId())
-                                                                                              .setCategory(productCategory)
-                                                                                              .setBundleStartDate(effectiveTransferDate)
-                                                                                              .setAlignStartDate(subscriptionAlignStartDate),
-                                                                                      ImmutableList.<SubscriptionBaseEvent>of());
+                                                                                                            .setId(UUID.randomUUID())
+                                                                                                            .setBundleId(subscriptionBundleData.getId())
+                                                                                                            .setCategory(productCategory)
+                                                                                                            .setBundleStartDate(effectiveTransferDate)
+                                                                                                            .setAlignStartDate(subscriptionAlignStartDate),
+                                                                                                    ImmutableList.<SubscriptionBaseEvent>of());
 
                 final List<SubscriptionBaseEvent> events = toEvents(existingEvents, defaultSubscriptionBase, effectiveTransferDate, context);
                 final SubscriptionMigrationData curData = new SubscriptionMigrationData(defaultSubscriptionBase, events, null);