killbill-aplcache

entitlement: refresh object state before r/w calls Make

11/2/2013 8:19:27 AM

Details

diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEntitlement.java
index 29b0d6a..03e6365 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEntitlement.java
@@ -25,6 +25,10 @@ import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
 
 import com.ning.billing.ErrorCode;
+import com.ning.billing.account.api.Account;
+import com.ning.billing.account.api.AccountApiException;
+import com.ning.billing.account.api.AccountInternalApi;
+import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.catalog.api.BillingActionPolicy;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Plan;
@@ -36,13 +40,14 @@ import com.ning.billing.clock.Clock;
 import com.ning.billing.entitlement.EntitlementService;
 import com.ning.billing.entitlement.block.BlockingChecker;
 import com.ning.billing.entitlement.dao.BlockingStateDao;
+import com.ning.billing.entity.EntityBase;
+import com.ning.billing.junction.DefaultBlockingState;
 import com.ning.billing.subscription.api.SubscriptionBase;
+import com.ning.billing.subscription.api.SubscriptionBaseInternalApi;
 import com.ning.billing.subscription.api.user.SubscriptionBaseApiException;
 import com.ning.billing.util.callcontext.CallContext;
-import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
-import com.ning.billing.entity.EntityBase;
-import com.ning.billing.junction.DefaultBlockingState;
+import com.ning.billing.util.callcontext.TenantContext;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
@@ -50,21 +55,25 @@ import com.google.common.collect.Collections2;
 public class DefaultEntitlement extends EntityBase implements Entitlement {
 
     protected final EntitlementDateHelper dateHelper;
-    protected final SubscriptionBase subscriptionBase;
     protected final InternalCallContextFactory internalCallContextFactory;
     protected final Clock clock;
-    protected final EntitlementState state;
-    protected final LocalDate effectiveEndDate;
     protected final BlockingChecker checker;
     protected final UUID accountId;
-    protected final String externalKey;
+    protected final AccountInternalApi accountApi;
     protected final EntitlementApi entitlementApi;
-    protected final DateTimeZone accountTimeZone;
+    protected final SubscriptionBaseInternalApi subscriptionInternalApi;
     protected final BlockingStateDao blockingStateDao;
 
+    // Refresh-able
+    protected SubscriptionBase subscriptionBase;
+    protected EntitlementState state;
+    protected LocalDate effectiveEndDate;
+    protected String externalKey;
+    protected DateTimeZone accountTimeZone;
+
     public DefaultEntitlement(final EntitlementDateHelper dateHelper, final SubscriptionBase subscriptionBase, final UUID accountId,
                               final String externalKey, final EntitlementState state, final LocalDate effectiveEndDate, final DateTimeZone accountTimeZone,
-                              final EntitlementApi entitlementApi, final InternalCallContextFactory internalCallContextFactory,
+                              final AccountInternalApi accountApi, final EntitlementApi entitlementApi, final SubscriptionBaseInternalApi subscriptionInternalApi, final InternalCallContextFactory internalCallContextFactory,
                               final BlockingStateDao blockingStateDao,
                               final Clock clock, final BlockingChecker checker) {
         super(subscriptionBase.getId(), subscriptionBase.getCreatedDate(), subscriptionBase.getUpdatedDate());
@@ -74,7 +83,9 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
         this.externalKey = externalKey;
         this.state = state;
         this.effectiveEndDate = effectiveEndDate;
+        this.accountApi = accountApi;
         this.entitlementApi = entitlementApi;
+        this.subscriptionInternalApi = subscriptionInternalApi;
         this.accountTimeZone = accountTimeZone;
         this.internalCallContextFactory = internalCallContextFactory;
         this.clock = clock;
@@ -90,10 +101,13 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
              in.getState(),
              in.getEffectiveEndDate(),
              in.getAccountTimeZone(),
+             in.getAccountApi(),
              in.getEntitlementApi(),
+             in.getSubscriptionInternalApi(),
              in.getInternalCallContextFactory(),
              in.getBlockingStateDao(),
-             in.getClock(), in.getChecker());
+             in.getClock(),
+             in.getChecker());
     }
 
     public SubscriptionBase getSubscriptionBase() {
@@ -108,10 +122,18 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
         return internalCallContextFactory;
     }
 
+    public AccountInternalApi getAccountApi() {
+        return accountApi;
+    }
+
     public EntitlementApi getEntitlementApi() {
         return entitlementApi;
     }
 
+    public SubscriptionBaseInternalApi getSubscriptionInternalApi() {
+        return subscriptionInternalApi;
+    }
+
     public Clock getClock() {
         return clock;
     }
@@ -202,11 +224,15 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
     @Override
     public Entitlement cancelEntitlementWithDate(final LocalDate localCancelDate, final boolean overrideBillingEffectiveDate, final CallContext callContext) throws EntitlementApiException {
+        final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
+        // Get the latest state from disk
+        refresh(callContext, contextWithValidAccountRecordId);
 
         if (state == EntitlementState.CANCELLED) {
             throw new EntitlementApiException(ErrorCode.SUB_CANCEL_BAD_STATE, getId(), EntitlementState.CANCELLED);
         }
-        final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
         final DateTime effectiveCancelDate = dateHelper.fromLocalDateAndReferenceTime(localCancelDate, subscriptionBase.getStartDate(), contextWithValidAccountRecordId);
         try {
             if (overrideBillingEffectiveDate) {
@@ -230,10 +256,14 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
     @Override
     public void uncancelEntitlement(final CallContext callContext) throws EntitlementApiException {
+        final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
+        // Get the latest state from disk
+        refresh(callContext, contextWithValidAccountRecordId);
+
         if (state == EntitlementState.CANCELLED || subscriptionBase.getState() == EntitlementState.CANCELLED) {
             throw new EntitlementApiException(ErrorCode.SUB_CANCEL_BAD_STATE, getId(), EntitlementState.CANCELLED);
         }
-        final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
         final List<BlockingState> blockingStatesForAccount = blockingStateDao.getBlockingAllForAccountRecordId(contextWithValidAccountRecordId);
         final Collection<BlockingState> futureEntitlementCancellationEvents = Collections2.filter(blockingStatesForAccount, new Predicate<BlockingState>() {
             @Override
@@ -274,11 +304,14 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
     @Override
     public Entitlement cancelEntitlementWithDateOverrideBillingPolicy(final LocalDate localCancelDate, final BillingActionPolicy billingPolicy, final CallContext callContext) throws EntitlementApiException {
+        final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
+        // Get the latest state from disk
+        refresh(callContext, contextWithValidAccountRecordId);
 
         if (state == EntitlementState.CANCELLED) {
             throw new EntitlementApiException(ErrorCode.SUB_CANCEL_BAD_STATE, getId(), EntitlementState.CANCELLED);
         }
-        final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
         final LocalDate effectiveLocalDate = new LocalDate(localCancelDate, accountTimeZone);
         final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(effectiveLocalDate, subscriptionBase.getStartDate(), contextWithValidAccountRecordId);
         try {
@@ -307,13 +340,16 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
 
     @Override
-    public Entitlement changePlan(final String productName, final BillingPeriod billingPeriod, final String priceList,final CallContext callContext) throws EntitlementApiException {
+    public Entitlement changePlan(final String productName, final BillingPeriod billingPeriod, final String priceList, final CallContext callContext) throws EntitlementApiException {
+        final InternalCallContext context = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
+        // Get the latest state from disk
+        refresh(callContext, context);
 
         if (state != EntitlementState.ACTIVE) {
             throw new EntitlementApiException(ErrorCode.SUB_CHANGE_NON_ACTIVE, getId(), state);
         }
 
-        final InternalCallContext context = internalCallContextFactory.createInternalCallContext(accountId, callContext);
         try {
             checker.checkBlockedChange(subscriptionBase, context);
             subscriptionBase.changePlan(productName, billingPeriod, priceList, callContext);
@@ -328,12 +364,15 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
     @Override
     public Entitlement changePlanWithDate(final String productName, final BillingPeriod billingPeriod, final String priceList, final LocalDate localDate, final CallContext callContext) throws EntitlementApiException {
+        final InternalCallContext context = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
+        // Get the latest state from disk
+        refresh(callContext, context);
 
         if (state != EntitlementState.ACTIVE) {
             throw new EntitlementApiException(ErrorCode.SUB_CHANGE_NON_ACTIVE, getId(), state);
         }
 
-        final InternalCallContext context = internalCallContextFactory.createInternalCallContext(accountId, callContext);
         try {
             checker.checkBlockedChange(subscriptionBase, context);
             final DateTime effectiveChangeDate = dateHelper.fromLocalDateAndReferenceTime(localDate, subscriptionBase.getStartDate(), context);
@@ -348,12 +387,15 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
     @Override
     public Entitlement changePlanOverrideBillingPolicy(final String productName, final BillingPeriod billingPeriod, final String priceList, final LocalDate localDateX, final BillingActionPolicy actionPolicy, final CallContext callContext) throws EntitlementApiException {
+        final InternalCallContext context = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
+        // Get the latest state from disk
+        refresh(callContext, context);
 
         if (state != EntitlementState.ACTIVE) {
             throw new EntitlementApiException(ErrorCode.SUB_CHANGE_NON_ACTIVE, getId(), state);
         }
 
-        final InternalCallContext context = internalCallContextFactory.createInternalCallContext(accountId, callContext);
         try {
             checker.checkBlockedChange(subscriptionBase, context);
             subscriptionBase.changePlanWithPolicy(productName, billingPeriod, priceList, actionPolicy, callContext);
@@ -364,4 +406,29 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
             throw new EntitlementApiException(e);
         }
     }
+
+    private void refresh(final TenantContext context, final InternalCallContext internalCallContext) throws EntitlementApiException {
+        try {
+            final Account account = accountApi.getAccountById(accountId, internalCallContext);
+            // Not really required today as we don't allow timezone changes, but do it nonetheless
+            // in case we change our mind later (and so we don't get surprises...).
+            accountTimeZone = account.getTimeZone();
+        } catch (AccountApiException e) {
+            throw new EntitlementApiException(e);
+        }
+
+        final Entitlement refreshedEntitlement = entitlementApi.getEntitlementForId(getId(), context);
+        state = refreshedEntitlement.getState();
+        effectiveEndDate = refreshedEntitlement.getEffectiveEndDate();
+        externalKey = refreshedEntitlement.getExternalKey();
+        if (refreshedEntitlement instanceof DefaultEntitlement) {
+            subscriptionBase = ((DefaultEntitlement) refreshedEntitlement).getSubscriptionBase();
+        } else {
+            try {
+                subscriptionBase = subscriptionInternalApi.getBaseSubscription(refreshedEntitlement.getBundleId(), internalCallContext);
+            } catch (SubscriptionBaseApiException e) {
+                throw new EntitlementApiException(e);
+            }
+        }
+    }
 }
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 1452b63..e6ecd26 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
@@ -107,8 +107,8 @@ public class DefaultEntitlementApi implements EntitlementApi {
             final DateTime referenceTime = clock.getUTCNow();
             final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(effectiveDate, referenceTime, contextWithValidAccountRecordId);
             final SubscriptionBase subscription = subscriptionInternalApi.createSubscription(bundle.getId(), planPhaseSpecifier, requestedDate, contextWithValidAccountRecordId);
-            return new DefaultEntitlement(dateHelper, subscription, accountId, bundle.getExternalKey(), EntitlementState.ACTIVE, null, account.getTimeZone(), this,
-                                          internalCallContextFactory, blockingStateDao, clock, checker);
+            return new DefaultEntitlement(dateHelper, subscription, accountId, bundle.getExternalKey(), EntitlementState.ACTIVE, null, account.getTimeZone(), accountApi, this,
+                                          subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker);
         } catch (SubscriptionBaseApiException e) {
             throw new EntitlementApiException(e);
         } catch (AccountApiException e) {
@@ -146,7 +146,7 @@ public class DefaultEntitlementApi implements EntitlementApi {
             final SubscriptionBase subscription = subscriptionInternalApi.createSubscription(baseSubscription.getBundleId(), planPhaseSpecifier, requestedDate, context);
 
             return new DefaultEntitlement(dateHelper, subscription, bundle.getAccountId(), bundle.getExternalKey(), EntitlementState.ACTIVE, null, account.getTimeZone(),
-                                          this, internalCallContextFactory, blockingStateDao, clock, checker);
+                                          accountApi, this, subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker);
         } catch (SubscriptionBaseApiException e) {
             throw new EntitlementApiException(e);
         } catch (BlockingApiException e) {
@@ -187,7 +187,7 @@ public class DefaultEntitlementApi implements EntitlementApi {
 
 
             return new DefaultEntitlement(dateHelper, subscription, bundle.getAccountId(), bundle.getExternalKey(), entitlementState, entitlementEffectiveEndDate, account.getTimeZone(),
-                                          this, internalCallContextFactory, blockingStateDao, clock, checker);
+                                          accountApi, this, subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker);
         } catch (SubscriptionBaseApiException e) {
             throw new EntitlementApiException(e);
         } catch (AccountApiException e) {
@@ -278,8 +278,8 @@ public class DefaultEntitlementApi implements EntitlementApi {
                                               entitlementState,
                                               effectiveEndDate,
                                               accountTimeZone,
-                                              thisEntitlementApi,
-                                              internalCallContextFactory, blockingStateDao, clock, checker);
+                                              accountApi, thisEntitlementApi,
+                                              subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker);
             }
         }));
     }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultEntitlementApi.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultEntitlementApi.java
index 99e35ee..1a8c158 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultEntitlementApi.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultEntitlementApi.java
@@ -48,6 +48,64 @@ import static org.testng.Assert.assertTrue;
 
 public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedDB {
 
+    @Test(groups = "slow")
+    public void testCheckStaleStates() throws AccountApiException, EntitlementApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = accountApi.createAccount(getAccountData(7), callContext);
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        // Keep the same object for the whole test, to make sure we refresh its state before r/w calls
+        final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), initialDate, callContext);
+
+        // Add ADD_ON
+        // Keep the same object for the whole test, to make sure we refresh its state before r/w calls
+        final PlanPhaseSpecifier addOnSpec = new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final Entitlement addOnEntitlement = entitlementApi.addEntitlement(entitlement.getBundleId(), addOnSpec, initialDate, callContext);
+
+        /*
+        // TODO It looks like we don't check if there is a future cancellation. Maybe we should?
+        try {
+            entitlement.uncancelEntitlement(callContext);
+            Assert.fail("Entitlement hasn't been cancelled yet");
+        } catch (final EntitlementApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.SUB_CANCEL_BAD_STATE.getCode());
+        }*/
+
+        clock.addDays(3);
+
+        // Cancelling the base entitlement will cancel the add-on
+        entitlement.cancelEntitlementWithDateOverrideBillingPolicy(clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, callContext);
+
+        try {
+            entitlement.cancelEntitlementWithDateOverrideBillingPolicy(clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, callContext);
+            Assert.fail("Entitlement is already cancelled");
+        } catch (final EntitlementApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.SUB_CANCEL_BAD_STATE.getCode());
+        }
+
+        try {
+            addOnEntitlement.cancelEntitlementWithDateOverrideBillingPolicy(clock.getUTCToday(), BillingActionPolicy.IMMEDIATE, callContext);
+            Assert.fail("Add-On Entitlement is already cancelled");
+        } catch (final EntitlementApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.SUB_CANCEL_BAD_STATE.getCode());
+        }
+
+        try {
+            entitlement.uncancelEntitlement(callContext);
+            Assert.fail("Entitlement is already cancelled");
+        } catch (final EntitlementApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.SUB_CANCEL_BAD_STATE.getCode());
+        }
+
+        try {
+            addOnEntitlement.uncancelEntitlement(callContext);
+            Assert.fail("Add-On Entitlement is already cancelled");
+        } catch (final EntitlementApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.SUB_CANCEL_BAD_STATE.getCode());
+        }
+    }
 
     @Test(groups = "slow")
     public void testCreateEntitlementWithCheck() {