killbill-aplcache
Changes
entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultBlockingTransitionInternalEvent.java 1(+0 -1)
entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEffectiveEntitlementEvent.java 1(+0 -1)
entitlement/src/main/java/com/ning/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java 68(+5 -63)
entitlement/src/main/java/com/ning/billing/entitlement/dao/DefaultBlockingStateDao.java 151(+129 -22)
entitlement/src/main/java/com/ning/billing/entitlement/engine/core/BlockingTransitionNotificationKey.java 136(+136 -0)
entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementNotificationKey.java 96(+96 -0)
entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementNotificationKeyAction.java 22(+22 -0)
entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementUtils.java 296(+296 -0)
entitlement/src/test/java/com/ning/billing/entitlement/dao/TestDefaultBlockingStateDao.java 21(+21 -0)
entitlement/src/test/java/com/ning/billing/entitlement/engine/core/TestEntitlementUtils.java 294(+294 -0)
entitlement/src/test/java/com/ning/billing/entitlement/EntitlementTestSuiteWithEmbeddedDB.java 51(+36 -15)
entitlement/src/test/java/com/ning/billing/entitlement/glue/TestEntitlementModuleNoDB.java 20(+19 -1)
pom.xml 2(+1 -1)
subscription/src/main/java/com/ning/billing/subscription/api/SubscriptionBaseApiService.java 15(+9 -6)
subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBase.java 12(+6 -6)
Details
diff --git a/api/src/main/java/com/ning/billing/events/BlockingTransitionInternalEvent.java b/api/src/main/java/com/ning/billing/events/BlockingTransitionInternalEvent.java
index bb6cb7a..1b9b385 100644
--- a/api/src/main/java/com/ning/billing/events/BlockingTransitionInternalEvent.java
+++ b/api/src/main/java/com/ning/billing/events/BlockingTransitionInternalEvent.java
@@ -20,7 +20,8 @@ import java.util.UUID;
import com.ning.billing.entitlement.api.BlockingStateType;
-public interface BlockingTransitionInternalEvent extends BusInternalEvent {
+// Event for effective blocking state changes (not entitlement specific)
+public interface BlockingTransitionInternalEvent extends BusInternalEvent {
public UUID getBlockableId();
diff --git a/api/src/main/java/com/ning/billing/subscription/api/SubscriptionBase.java b/api/src/main/java/com/ning/billing/subscription/api/SubscriptionBase.java
index 21984c3..a75c5f8 100644
--- a/api/src/main/java/com/ning/billing/subscription/api/SubscriptionBase.java
+++ b/api/src/main/java/com/ning/billing/subscription/api/SubscriptionBase.java
@@ -53,14 +53,17 @@ public interface SubscriptionBase extends Entity, Blockable {
public boolean uncancel(final CallContext context)
throws SubscriptionBaseApiException;
- public boolean changePlan(final String productName, final BillingPeriod term, final String priceList, final CallContext context)
+ // Return the effective date of the change, null for immediate
+ public DateTime changePlan(final String productName, final BillingPeriod term, final String priceList, final CallContext context)
throws SubscriptionBaseApiException;
- public boolean changePlanWithDate(final String productName, final BillingPeriod term, final String priceList, final DateTime requestedDate, final CallContext context)
+ // Return the effective date of the change, null for immediate
+ public DateTime changePlanWithDate(final String productName, final BillingPeriod term, final String priceList, final DateTime requestedDate, final CallContext context)
throws SubscriptionBaseApiException;
- public boolean changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
- final BillingActionPolicy policy, final CallContext context)
+ // Return the effective date of the change, null for immediate
+ public DateTime changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+ final BillingActionPolicy policy, final CallContext context)
throws SubscriptionBaseApiException;
public UUID getBundleId();
diff --git a/catalog/src/main/java/com/ning/billing/catalog/DefaultProduct.java b/catalog/src/main/java/com/ning/billing/catalog/DefaultProduct.java
index 92873cd..66fb2b7 100644
--- a/catalog/src/main/java/com/ning/billing/catalog/DefaultProduct.java
+++ b/catalog/src/main/java/com/ning/billing/catalog/DefaultProduct.java
@@ -198,4 +198,52 @@ public class DefaultProduct extends ValidatingConfig<StandaloneCatalog> implemen
+ Arrays.toString(included) + ", available=" + Arrays.toString(available) + ", catalogName="
+ catalogName + "]";
}
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultProduct that = (DefaultProduct) o;
+
+ if (!Arrays.equals(available, that.available)) {
+ return false;
+ }
+ if (catalogName != null ? !catalogName.equals(that.catalogName) : that.catalogName != null) {
+ return false;
+ }
+ if (category != that.category) {
+ return false;
+ }
+ if (!Arrays.equals(included, that.included)) {
+ return false;
+ }
+ if (!Arrays.equals(limits, that.limits)) {
+ return false;
+ }
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+ if (retired != null ? !retired.equals(that.retired) : that.retired != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (retired != null ? retired.hashCode() : 0);
+ result = 31 * result + (category != null ? category.hashCode() : 0);
+ result = 31 * result + (included != null ? Arrays.hashCode(included) : 0);
+ result = 31 * result + (available != null ? Arrays.hashCode(available) : 0);
+ result = 31 * result + (limits != null ? Arrays.hashCode(limits) : 0);
+ result = 31 * result + (catalogName != null ? catalogName.hashCode() : 0);
+ return result;
+ }
}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultBlockingTransitionInternalEvent.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultBlockingTransitionInternalEvent.java
index 6d4efb4..93f898a 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultBlockingTransitionInternalEvent.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultBlockingTransitionInternalEvent.java
@@ -27,7 +27,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
public class DefaultBlockingTransitionInternalEvent extends BusEventBase implements BlockingTransitionInternalEvent {
-
private final UUID blockableId;
private final BlockingStateType blockingType;
private final Boolean isTransitionedToBlockedBilling;
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEffectiveEntitlementEvent.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEffectiveEntitlementEvent.java
index 38f233c..ff6908e 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEffectiveEntitlementEvent.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultEffectiveEntitlementEvent.java
@@ -38,7 +38,6 @@ public class DefaultEffectiveEntitlementEvent extends BusEventBase implements Ef
private final DateTime effectiveTransitionTime;
private final DateTime requestedTransitionTime;
-
@JsonCreator
public DefaultEffectiveEntitlementEvent(@JsonProperty("eventId") final UUID id,
@JsonProperty("entitlementId") final UUID entitlementId,
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 03e6365..099bd23 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
@@ -16,10 +16,13 @@
package com.ning.billing.entitlement.api;
+import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
+import javax.annotation.Nullable;
+
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
@@ -37,11 +40,19 @@ import com.ning.billing.catalog.api.PriceList;
import com.ning.billing.catalog.api.Product;
import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.clock.Clock;
+import com.ning.billing.entitlement.DefaultEntitlementService;
import com.ning.billing.entitlement.EntitlementService;
import com.ning.billing.entitlement.block.BlockingChecker;
import com.ning.billing.entitlement.dao.BlockingStateDao;
+import com.ning.billing.entitlement.engine.core.EntitlementNotificationKey;
+import com.ning.billing.entitlement.engine.core.EntitlementNotificationKeyAction;
+import com.ning.billing.entitlement.engine.core.EntitlementUtils;
import com.ning.billing.entity.EntityBase;
import com.ning.billing.junction.DefaultBlockingState;
+import com.ning.billing.notificationq.api.NotificationEvent;
+import com.ning.billing.notificationq.api.NotificationQueue;
+import com.ning.billing.notificationq.api.NotificationQueueService;
+import com.ning.billing.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
import com.ning.billing.subscription.api.SubscriptionBase;
import com.ning.billing.subscription.api.SubscriptionBaseInternalApi;
import com.ning.billing.subscription.api.user.SubscriptionBaseApiException;
@@ -63,6 +74,8 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
protected final EntitlementApi entitlementApi;
protected final SubscriptionBaseInternalApi subscriptionInternalApi;
protected final BlockingStateDao blockingStateDao;
+ protected final NotificationQueueService notificationQueueService;
+ protected final EntitlementUtils entitlementUtils;
// Refresh-able
protected SubscriptionBase subscriptionBase;
@@ -74,8 +87,8 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
public DefaultEntitlement(final EntitlementDateHelper dateHelper, final SubscriptionBase subscriptionBase, final UUID accountId,
final String externalKey, final EntitlementState state, final LocalDate effectiveEndDate, final DateTimeZone accountTimeZone,
final AccountInternalApi accountApi, final EntitlementApi entitlementApi, final SubscriptionBaseInternalApi subscriptionInternalApi, final InternalCallContextFactory internalCallContextFactory,
- final BlockingStateDao blockingStateDao,
- final Clock clock, final BlockingChecker checker) {
+ final BlockingStateDao blockingStateDao, final Clock clock, final BlockingChecker checker, final NotificationQueueService notificationQueueService,
+ final EntitlementUtils entitlementUtils) {
super(subscriptionBase.getId(), subscriptionBase.getCreatedDate(), subscriptionBase.getUpdatedDate());
this.dateHelper = dateHelper;
this.subscriptionBase = subscriptionBase;
@@ -91,6 +104,8 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
this.clock = clock;
this.checker = checker;
this.blockingStateDao = blockingStateDao;
+ this.notificationQueueService = notificationQueueService;
+ this.entitlementUtils = entitlementUtils;
}
public DefaultEntitlement(final DefaultEntitlement in) {
@@ -107,7 +122,9 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
in.getInternalCallContextFactory(),
in.getBlockingStateDao(),
in.getClock(),
- in.getChecker());
+ in.getChecker(),
+ in.getNotificationQueueService(),
+ in.getEntitlementUtils());
}
public SubscriptionBase getSubscriptionBase() {
@@ -150,6 +167,14 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
return blockingStateDao;
}
+ public NotificationQueueService getNotificationQueueService() {
+ return notificationQueueService;
+ }
+
+ public EntitlementUtils getEntitlementUtils() {
+ return entitlementUtils;
+ }
+
@Override
public UUID getBaseEntitlementId() {
return subscriptionBase.getId();
@@ -218,6 +243,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
@Override
public Entitlement cancelEntitlementWithPolicy(final EntitlementActionPolicy entitlementPolicy, final CallContext callContext) throws EntitlementApiException {
+ final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
+ // Get the latest state from disk - required to have the latest CTD
+ refresh(callContext, contextWithValidAccountRecordId);
+
final LocalDate cancellationDate = getLocalDateFromEntitlementPolicy(entitlementPolicy);
return cancelEntitlementWithDate(cancellationDate, false, callContext);
}
@@ -240,16 +270,25 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
} else {
subscriptionBase.cancel(callContext);
}
- blockingStateDao.setBlockingState(new DefaultBlockingState(getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, effectiveCancelDate), clock, contextWithValidAccountRecordId);
- return entitlementApi.getEntitlementForId(getId(), callContext);
} catch (SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
}
- }
+ final BlockingState newBlockingState = new DefaultBlockingState(getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, effectiveCancelDate);
+ entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(newBlockingState, contextWithValidAccountRecordId);
+
+ blockAddOnsIfRequired(effectiveCancelDate, callContext, contextWithValidAccountRecordId);
+
+ return entitlementApi.getEntitlementForId(getId(), callContext);
+ }
@Override
public Entitlement cancelEntitlementWithPolicyOverrideBillingPolicy(final EntitlementActionPolicy entitlementPolicy, final BillingActionPolicy billingPolicy, final CallContext callContext) throws EntitlementApiException {
+ final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
+ // Get the latest state from disk - required to have the latest CTD
+ refresh(callContext, contextWithValidAccountRecordId);
+
final LocalDate cancellationDate = getLocalDateFromEntitlementPolicy(entitlementPolicy);
return cancelEntitlementWithDateOverrideBillingPolicy(cancellationDate, billingPolicy, callContext);
}
@@ -312,15 +351,21 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
if (state == EntitlementState.CANCELLED) {
throw new EntitlementApiException(ErrorCode.SUB_CANCEL_BAD_STATE, getId(), EntitlementState.CANCELLED);
}
- final LocalDate effectiveLocalDate = new LocalDate(localCancelDate, accountTimeZone);
- final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(effectiveLocalDate, subscriptionBase.getStartDate(), contextWithValidAccountRecordId);
+
try {
subscriptionBase.cancelWithPolicy(billingPolicy, callContext);
- blockingStateDao.setBlockingState(new DefaultBlockingState(getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, effectiveDate), clock, contextWithValidAccountRecordId);
- return entitlementApi.getEntitlementForId(getId(), callContext);
} catch (SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
}
+
+ final LocalDate effectiveLocalDate = new LocalDate(localCancelDate, accountTimeZone);
+ final DateTime effectiveDate = dateHelper.fromLocalDateAndReferenceTime(effectiveLocalDate, subscriptionBase.getStartDate(), contextWithValidAccountRecordId);
+ final BlockingState newBlockingState = new DefaultBlockingState(getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, effectiveDate);
+ entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(newBlockingState, contextWithValidAccountRecordId);
+
+ blockAddOnsIfRequired(effectiveDate, callContext, contextWithValidAccountRecordId);
+
+ return entitlementApi.getEntitlementForId(getId(), callContext);
}
private LocalDate getLocalDateFromEntitlementPolicy(final EntitlementActionPolicy entitlementPolicy) {
@@ -338,7 +383,6 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
return cancellationDate;
}
-
@Override
public Entitlement changePlan(final String productName, final BillingPeriod billingPeriod, final String priceList, final CallContext callContext) throws EntitlementApiException {
final InternalCallContext context = internalCallContextFactory.createInternalCallContext(accountId, callContext);
@@ -352,15 +396,21 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
try {
checker.checkBlockedChange(subscriptionBase, context);
- subscriptionBase.changePlan(productName, billingPeriod, priceList, callContext);
- return entitlementApi.getEntitlementForId(getId(), callContext);
} catch (BlockingApiException e) {
throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+ }
+
+ final DateTime effectiveChangeDate;
+ try {
+ effectiveChangeDate = subscriptionBase.changePlan(productName, billingPeriod, priceList, callContext);
} catch (SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
}
- }
+ blockAddOnsIfRequired(effectiveChangeDate, callContext, context);
+
+ return entitlementApi.getEntitlementForId(getId(), callContext);
+ }
@Override
public Entitlement changePlanWithDate(final String productName, final BillingPeriod billingPeriod, final String priceList, final LocalDate localDate, final CallContext callContext) throws EntitlementApiException {
@@ -375,14 +425,20 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
try {
checker.checkBlockedChange(subscriptionBase, context);
- final DateTime effectiveChangeDate = dateHelper.fromLocalDateAndReferenceTime(localDate, subscriptionBase.getStartDate(), context);
- subscriptionBase.changePlanWithDate(productName, billingPeriod, priceList, effectiveChangeDate, callContext);
- return entitlementApi.getEntitlementForId(getId(), callContext);
} catch (BlockingApiException e) {
throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+ }
+
+ final DateTime effectiveChangeDate = dateHelper.fromLocalDateAndReferenceTime(localDate, subscriptionBase.getStartDate(), context);
+ try {
+ subscriptionBase.changePlanWithDate(productName, billingPeriod, priceList, effectiveChangeDate, callContext);
} catch (SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
}
+
+ blockAddOnsIfRequired(effectiveChangeDate, callContext, context);
+
+ return entitlementApi.getEntitlementForId(getId(), callContext);
}
@Override
@@ -398,13 +454,20 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
try {
checker.checkBlockedChange(subscriptionBase, context);
- subscriptionBase.changePlanWithPolicy(productName, billingPeriod, priceList, actionPolicy, callContext);
- return entitlementApi.getEntitlementForId(getId(), callContext);
} catch (BlockingApiException e) {
throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+ }
+
+ final DateTime effectiveChangeDate;
+ try {
+ effectiveChangeDate = subscriptionBase.changePlanWithPolicy(productName, billingPeriod, priceList, actionPolicy, callContext);
} catch (SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
}
+
+ blockAddOnsIfRequired(effectiveChangeDate, callContext, context);
+
+ return entitlementApi.getEntitlementForId(getId(), callContext);
}
private void refresh(final TenantContext context, final InternalCallContext internalCallContext) throws EntitlementApiException {
@@ -431,4 +494,53 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
}
}
}
+
+ public void blockAddOnsIfRequired(@Nullable final DateTime effectiveDateOrNull, final TenantContext context, final InternalCallContext internalCallContext) throws EntitlementApiException {
+ // Optimization - bail early
+ if (!ProductCategory.BASE.equals(subscriptionBase.getCategory())) {
+ // Only base subscriptions have add-ons
+ return;
+ }
+
+ // Get the latest state from disk (we just got cancelled or changed plan)
+ refresh(context, internalCallContext);
+
+ // null means immediate
+ final DateTime effectiveDate = effectiveDateOrNull == null ? clock.getUTCNow() : effectiveDateOrNull;
+
+ final boolean isBaseEntitlementCancelled = EntitlementState.CANCELLED.equals(state);
+
+ // If cancellation/change occurs in the future, do nothing for now but add a notification entry.
+ // This is to distinguish whether a future cancellation was requested by the user, or was a side effect
+ // (e.g. base plan cancellation): future entitlement cancellations for add-ons on disk always reflect
+ // an explicit cancellation. This trick lets us determine what to do when un-cancelling.
+ // This mirror the behavior in subscription base (see DefaultSubscriptionBaseApiService).
+ final DateTime now = clock.getUTCNow();
+ if (effectiveDate.compareTo(now) > 0) {
+ // Note that usually we record the notification from the DAO. We cannot do it here because not all calls
+ // go through the DAO (e.g. change)
+ final NotificationEvent notificationEvent = new EntitlementNotificationKey(getId(), isBaseEntitlementCancelled ? EntitlementNotificationKeyAction.CANCEL : EntitlementNotificationKeyAction.CHANGE, effectiveDate);
+ recordFutureNotification(effectiveDate, notificationEvent, internalCallContext);
+ return;
+ }
+
+ final Collection<BlockingState> addOnsBlockingStates = entitlementUtils.computeBlockingStatesForAssociatedAddons(subscriptionBase, effectiveDate, internalCallContext);
+ for (final BlockingState addOnBlockingState : addOnsBlockingStates) {
+ entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(addOnBlockingState, internalCallContext);
+ }
+ }
+
+ private void recordFutureNotification(final DateTime effectiveDate,
+ final NotificationEvent notificationEvent,
+ final InternalCallContext context) {
+ try {
+ final NotificationQueue subscriptionEventQueue = notificationQueueService.getNotificationQueue(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
+ DefaultEntitlementService.NOTIFICATION_QUEUE_NAME);
+ subscriptionEventQueue.recordFutureNotification(effectiveDate, notificationEvent, context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
+ } catch (NoSuchNotificationQueue e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(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 e6ecd26..c3cc501 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
@@ -35,8 +35,11 @@ import com.ning.billing.ErrorCode;
import com.ning.billing.ObjectType;
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.bus.api.PersistentBus;
import com.ning.billing.bus.api.PersistentBus.EventBusException;
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.callcontext.InternalTenantContext;
import com.ning.billing.catalog.api.BillingActionPolicy;
import com.ning.billing.catalog.api.PlanPhaseSpecifier;
import com.ning.billing.catalog.api.ProductCategory;
@@ -47,19 +50,18 @@ import com.ning.billing.entitlement.api.Entitlement.EntitlementState;
import com.ning.billing.entitlement.block.BlockingChecker;
import com.ning.billing.entitlement.block.BlockingChecker.BlockingAggregator;
import com.ning.billing.entitlement.dao.BlockingStateDao;
+import com.ning.billing.entitlement.engine.core.EntitlementUtils;
+import com.ning.billing.junction.DefaultBlockingState;
+import com.ning.billing.notificationq.api.NotificationQueueService;
import com.ning.billing.subscription.api.SubscriptionBase;
+import com.ning.billing.subscription.api.SubscriptionBaseInternalApi;
import com.ning.billing.subscription.api.transfer.SubscriptionBaseTransferApi;
import com.ning.billing.subscription.api.transfer.SubscriptionBaseTransferApiException;
import com.ning.billing.subscription.api.user.SubscriptionBaseApiException;
import com.ning.billing.subscription.api.user.SubscriptionBaseBundle;
import com.ning.billing.util.callcontext.CallContext;
-import com.ning.billing.callcontext.InternalCallContext;
import com.ning.billing.util.callcontext.InternalCallContextFactory;
-import com.ning.billing.callcontext.InternalTenantContext;
import com.ning.billing.util.callcontext.TenantContext;
-import com.ning.billing.account.api.AccountInternalApi;
-import com.ning.billing.junction.DefaultBlockingState;
-import com.ning.billing.subscription.api.SubscriptionBaseInternalApi;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
@@ -82,9 +84,14 @@ public class DefaultEntitlementApi implements EntitlementApi {
private final BlockingStateDao blockingStateDao;
private final EntitlementDateHelper dateHelper;
private final PersistentBus eventBus;
+ protected final NotificationQueueService notificationQueueService;
+ protected final EntitlementUtils entitlementUtils;
@Inject
- public DefaultEntitlementApi(final PersistentBus eventBus, final InternalCallContextFactory internalCallContextFactory, final SubscriptionBaseTransferApi subscriptionTransferApi, final SubscriptionBaseInternalApi subscriptionInternalApi, final AccountInternalApi accountApi, final BlockingStateDao blockingStateDao, final Clock clock, final BlockingChecker checker) {
+ public DefaultEntitlementApi(final PersistentBus eventBus, final InternalCallContextFactory internalCallContextFactory,
+ final SubscriptionBaseTransferApi subscriptionTransferApi, final SubscriptionBaseInternalApi subscriptionInternalApi,
+ final AccountInternalApi accountApi, final BlockingStateDao blockingStateDao, final Clock clock,
+ final BlockingChecker checker, final NotificationQueueService notificationQueueService, final EntitlementUtils entitlementUtils) {
this.eventBus = eventBus;
this.internalCallContextFactory = internalCallContextFactory;
this.subscriptionInternalApi = subscriptionInternalApi;
@@ -93,10 +100,11 @@ public class DefaultEntitlementApi implements EntitlementApi {
this.clock = clock;
this.checker = checker;
this.blockingStateDao = blockingStateDao;
+ this.notificationQueueService = notificationQueueService;
+ this.entitlementUtils = entitlementUtils;
this.dateHelper = new EntitlementDateHelper(accountApi, clock);
}
-
@Override
public Entitlement createBaseEntitlement(final UUID accountId, final PlanPhaseSpecifier planPhaseSpecifier, final String externalKey, final LocalDate effectiveDate, final CallContext callContext) throws EntitlementApiException {
final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
@@ -108,7 +116,7 @@ public class DefaultEntitlementApi implements EntitlementApi {
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(), accountApi, this,
- subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker);
+ subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker, notificationQueueService, entitlementUtils);
} catch (SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
} catch (AccountApiException e) {
@@ -146,7 +154,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(),
- accountApi, this, subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker);
+ accountApi, this, subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker, notificationQueueService, entitlementUtils);
} catch (SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
} catch (BlockingApiException e) {
@@ -187,7 +195,7 @@ public class DefaultEntitlementApi implements EntitlementApi {
return new DefaultEntitlement(dateHelper, subscription, bundle.getAccountId(), bundle.getExternalKey(), entitlementState, entitlementEffectiveEndDate, account.getTimeZone(),
- accountApi, this, subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker);
+ accountApi, this, subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker, notificationQueueService, entitlementUtils);
} catch (SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
} catch (AccountApiException e) {
@@ -279,7 +287,8 @@ public class DefaultEntitlementApi implements EntitlementApi {
effectiveEndDate,
accountTimeZone,
accountApi, thisEntitlementApi,
- subscriptionInternalApi, internalCallContextFactory, blockingStateDao, clock, checker);
+ subscriptionInternalApi, internalCallContextFactory,
+ blockingStateDao, clock, checker, notificationQueueService, entitlementUtils);
}
}));
}
@@ -348,8 +357,8 @@ public class DefaultEntitlementApi implements EntitlementApi {
throw new UnsupportedOperationException("Pausing with a future date has not been implemented yet");
}
- final DefaultBlockingState state = new DefaultBlockingState(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, ENT_STATE_BLOCKED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, true, effectiveDate);
- blockingStateDao.setBlockingState(state, clock, contextWithValidAccountRecordId);
+ final BlockingState state = new DefaultBlockingState(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, ENT_STATE_BLOCKED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, true, effectiveDate);
+ entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(state, contextWithValidAccountRecordId);
// Should we send one event per entitlement in the bundle?
// Code below only sends one event for the bundle and use the base entitlementId
@@ -391,8 +400,8 @@ public class DefaultEntitlementApi implements EntitlementApi {
throw new UnsupportedOperationException("Resuming with a future date has not been implemented yet");
}
- final DefaultBlockingState state = new DefaultBlockingState(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, ENT_STATE_CLEAR, EntitlementService.ENTITLEMENT_SERVICE_NAME, false, false, false, effectiveDate);
- blockingStateDao.setBlockingState(state, clock, contextWithValidAccountRecordId);
+ final BlockingState state = new DefaultBlockingState(bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, ENT_STATE_CLEAR, EntitlementService.ENTITLEMENT_SERVICE_NAME, false, false, false, effectiveDate);
+ entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(state, contextWithValidAccountRecordId);
// Should we send one event per entitlement in the bundle?
// Code below only sends one event for the bundle and use the base entitlementId
@@ -445,7 +454,8 @@ 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);
- blockingStateDao.setBlockingState(new DefaultBlockingState(bundle.getId(), BlockingStateType.SUBSCRIPTION_BUNDLE, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, requestedDate), clock, contextWithValidAccountRecordId);
+ 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);
return newBundle.getId();
} catch (SubscriptionBaseTransferApiException e) {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java
index 6cc1cd7..ea858dd 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/svcs/DefaultInternalBlockingApi.java
@@ -19,24 +19,15 @@ package com.ning.billing.entitlement.api.svcs;
import java.util.List;
import java.util.UUID;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import com.ning.billing.account.api.Account;
-import com.ning.billing.bus.api.PersistentBus;
-import com.ning.billing.bus.api.PersistentBus.EventBusException;
import com.ning.billing.callcontext.InternalCallContext;
import com.ning.billing.callcontext.InternalTenantContext;
import com.ning.billing.clock.Clock;
import com.ning.billing.entitlement.api.Blockable;
-import com.ning.billing.entitlement.api.BlockingApiException;
import com.ning.billing.entitlement.api.BlockingState;
import com.ning.billing.entitlement.api.BlockingStateType;
-import com.ning.billing.entitlement.api.DefaultBlockingTransitionInternalEvent;
-import com.ning.billing.entitlement.block.BlockingChecker;
-import com.ning.billing.entitlement.block.BlockingChecker.BlockingAggregator;
import com.ning.billing.entitlement.dao.BlockingStateDao;
-import com.ning.billing.events.BlockingTransitionInternalEvent;
+import com.ning.billing.entitlement.engine.core.EntitlementUtils;
import com.ning.billing.junction.BlockingInternalApi;
import com.ning.billing.junction.DefaultBlockingState;
@@ -44,20 +35,15 @@ import com.google.inject.Inject;
public class DefaultInternalBlockingApi implements BlockingInternalApi {
-
- private static final Logger log = LoggerFactory.getLogger(DefaultInternalBlockingApi.class);
-
+ private final EntitlementUtils entitlementUtils;
private final BlockingStateDao dao;
- private final BlockingChecker blockingChecker;
private final Clock clock;
- private final PersistentBus eventBus;
@Inject
- public DefaultInternalBlockingApi(final BlockingStateDao dao, final BlockingChecker blockingChecker, final PersistentBus eventBus, final Clock clock) {
+ public DefaultInternalBlockingApi(final EntitlementUtils entitlementUtils, final BlockingStateDao dao, final Clock clock) {
+ this.entitlementUtils = entitlementUtils;
this.dao = dao;
this.clock = clock;
- this.blockingChecker = blockingChecker;
- this.eventBus = eventBus;
}
@Override
@@ -96,53 +82,9 @@ public class DefaultInternalBlockingApi implements BlockingInternalApi {
@Override
public void setBlockingState(final BlockingState state, final InternalCallContext context) {
-
- final BlockingAggregator previousState = getBlockingStateFor(state.getBlockedId(), state.getType(), context);
-
- dao.setBlockingState(state, clock, context);
-
- final BlockingAggregator currentState = getBlockingStateFor(state.getBlockedId(), state.getType(), context);
- if (previousState != null && currentState != null) {
- postBlockingTransitionEvent(state.getBlockedId(), state.getType(), previousState, currentState, context);
- }
- }
-
- private BlockingAggregator getBlockingStateFor(final UUID blockableId, final BlockingStateType type, final InternalCallContext context) {
- try {
- return blockingChecker.getBlockedStatus(blockableId, type, context);
- } catch (BlockingApiException e) {
- log.warn("Failed to retrieve blocking state for {} {}", blockableId, type);
- return null;
- }
+ entitlementUtils.setBlockingStateAndPostBlockingTransitionEvent(state, context);
}
- private void postBlockingTransitionEvent(final UUID blockableId, final BlockingStateType type,
- final BlockingAggregator previousState, final BlockingAggregator currentState, final InternalCallContext context) {
-
- try {
- final boolean isTransitionToBlockedBilling = !previousState.isBlockBilling() && currentState.isBlockBilling();
- final boolean isTransitionToUnblockedBilling = previousState.isBlockBilling() && !currentState.isBlockBilling();
-
- final boolean isTransitionToBlockedEntitlement = !previousState.isBlockEntitlement() && currentState.isBlockEntitlement();
- final boolean isTransitionToUnblockedEntitlement = previousState.isBlockEntitlement() && !currentState.isBlockEntitlement();
-
- final BlockingTransitionInternalEvent event = new DefaultBlockingTransitionInternalEvent(blockableId, type,
- isTransitionToBlockedBilling, isTransitionToUnblockedBilling,
- isTransitionToBlockedEntitlement, isTransitionToUnblockedEntitlement,
-
- context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
-
- // TODO
- // STEPH Ideally we would like to post from transaction when we inserted the new blocking state, but new state would have to be recalculated from transaction which is
- // difficult without the help of BlockingChecker -- which itself relies on dao. Other alternative is duplicating the logic, or refactoring the DAO to export higher level api.
- eventBus.post(event);
- } catch (EventBusException e) {
- log.warn("Failed to post event {}", e);
- }
-
- }
-
-
BlockingStateType getBlockingStateType(final Blockable overdueable) {
if (overdueable instanceof Account) {
return BlockingStateType.ACCOUNT;
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/dao/BlockingStateModelDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/dao/BlockingStateModelDao.java
index 611d551..de4e3da 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/dao/BlockingStateModelDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/dao/BlockingStateModelDao.java
@@ -54,9 +54,13 @@ public class BlockingStateModelDao extends EntityBase implements EntityModelDao<
this.isActive = isActive;
}
- public BlockingStateModelDao(final BlockingState src, InternalCallContext context) {
+ public BlockingStateModelDao(final BlockingState src, final InternalCallContext context) {
+ this(src, context.getCreatedDate(), context.getUpdatedDate());
+ }
+
+ public BlockingStateModelDao(final BlockingState src, final DateTime createdDate, final DateTime updatedDate) {
this(src.getId(), src.getBlockedId(), src.getType(), src.getStateName(), src.getService(), src.isBlockChange(),
- src.isBlockEntitlement(), src.isBlockBilling(), src.getEffectiveDate(), true, context.getCreatedDate(), context.getUpdatedDate());
+ src.isBlockEntitlement(), src.isBlockBilling(), src.getEffectiveDate(), true, createdDate, updatedDate);
}
public UUID getBlockableId() {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/dao/DefaultBlockingStateDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/dao/DefaultBlockingStateDao.java
index c81784b..7ea44e1 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/dao/DefaultBlockingStateDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/dao/DefaultBlockingStateDao.java
@@ -19,19 +19,32 @@ package com.ning.billing.entitlement.dao;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
+import java.util.Date;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.inject.Inject;
+import org.joda.time.DateTime;
import org.skife.jdbi.v2.IDBI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.ning.billing.callcontext.InternalCallContext;
import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.clock.Clock;
+import com.ning.billing.entitlement.EntitlementService;
import com.ning.billing.entitlement.api.BlockingState;
+import com.ning.billing.entitlement.api.Entitlement.EntitlementState;
+import com.ning.billing.entitlement.api.EntitlementApiException;
+import com.ning.billing.entitlement.engine.core.EntitlementUtils;
+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.cache.CacheControllerDispatcher;
import com.ning.billing.util.dao.NonEntityDao;
import com.ning.billing.util.entity.dao.EntitySqlDao;
@@ -40,16 +53,42 @@ import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
import com.ning.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
import com.google.common.base.Function;
+import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
public class DefaultBlockingStateDao implements BlockingStateDao {
+ private static final Ordering<BlockingStateModelDao> BLOCKING_STATE_MODEL_DAO_ORDERING = Ordering.<BlockingStateModelDao>from(new Comparator<BlockingStateModelDao>() {
+ @Override
+ public int compare(final BlockingStateModelDao o1, final BlockingStateModelDao o2) {
+ // effective_date column NOT NULL
+ final int comparison = o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
+ if (comparison == 0) {
+ // Keep a stable ordering for ties
+ return o1.getCreatedDate().compareTo(o2.getCreatedDate());
+ } else {
+ return comparison;
+ }
+ }
+ });
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultBlockingStateDao.class);
+
+ // Lame to rely on the API at this (low) level, but we need information from subscription to insert events not on disk
+ private final SubscriptionBaseInternalApi subscriptionInternalApi;
private final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
private final Clock clock;
+ private final EntitlementUtils entitlementUtils;
@Inject
- public DefaultBlockingStateDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ public DefaultBlockingStateDao(final SubscriptionBaseInternalApi subscriptionBaseInternalApi, final IDBI dbi, final Clock clock,
+ final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao,
+ final EntitlementUtils entitlementUtils) {
+ this.subscriptionInternalApi = subscriptionBaseInternalApi;
+ this.entitlementUtils = entitlementUtils;
this.transactionalSqlDao = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao);
this.clock = clock;
}
@@ -59,7 +98,9 @@ public class DefaultBlockingStateDao implements BlockingStateDao {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<BlockingState>() {
@Override
public BlockingState inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
- final BlockingStateModelDao model = entitySqlDaoWrapperFactory.become(BlockingStateSqlDao.class).getBlockingStateForService(blockableId, serviceName, clock.getUTCNow().toDate(), context);
+ // Upper bound time limit is now
+ final Date upTo = clock.getUTCNow().toDate();
+ final BlockingStateModelDao model = entitySqlDaoWrapperFactory.become(BlockingStateSqlDao.class).getBlockingStateForService(blockableId, serviceName, upTo, context);
return BlockingStateModelDao.toBlockingState(model);
}
@@ -71,7 +112,9 @@ public class DefaultBlockingStateDao implements BlockingStateDao {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<BlockingState>>() {
@Override
public List<BlockingState> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
- final List<BlockingStateModelDao> models = entitySqlDaoWrapperFactory.become(BlockingStateSqlDao.class).getBlockingState(blockableId, clock.getUTCNow().toDate(), context);
+ // Upper bound time limit is now
+ final Date upTo = clock.getUTCNow().toDate();
+ final List<BlockingStateModelDao> models = entitySqlDaoWrapperFactory.become(BlockingStateSqlDao.class).getBlockingState(blockableId, upTo, context);
return new ArrayList<BlockingState>(Collections2.transform(models, new Function<BlockingStateModelDao, BlockingState>() {
@Override
public BlockingState apply(@Nullable final BlockingStateModelDao src) {
@@ -87,8 +130,10 @@ public class DefaultBlockingStateDao implements BlockingStateDao {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<BlockingState>>() {
@Override
public List<BlockingState> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
- final List<BlockingStateModelDao> models = entitySqlDaoWrapperFactory.become(BlockingStateSqlDao.class).getBlockingHistoryForService(blockableId, serviceName, context);
- return new ArrayList<BlockingState>(Collections2.transform(models, new Function<BlockingStateModelDao, BlockingState>() {
+ final BlockingStateSqlDao sqlDao = entitySqlDaoWrapperFactory.become(BlockingStateSqlDao.class);
+ final List<BlockingStateModelDao> modelsOnDisk = sqlDao.getBlockingHistoryForService(blockableId, serviceName, context);
+ final List<BlockingStateModelDao> allModels = addBlockingStatesNotOnDisk(blockableId, modelsOnDisk, sqlDao, context);
+ return new ArrayList<BlockingState>(Collections2.transform(allModels, new Function<BlockingStateModelDao, BlockingState>() {
@Override
public BlockingState apply(@Nullable final BlockingStateModelDao src) {
return BlockingStateModelDao.toBlockingState(src);
@@ -103,8 +148,10 @@ public class DefaultBlockingStateDao implements BlockingStateDao {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<BlockingState>>() {
@Override
public List<BlockingState> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
- final List<BlockingStateModelDao> models = entitySqlDaoWrapperFactory.become(BlockingStateSqlDao.class).getBlockingAll(blockableId, context);
- return new ArrayList<BlockingState>(Collections2.transform(models, new Function<BlockingStateModelDao, BlockingState>() {
+ final BlockingStateSqlDao sqlDao = entitySqlDaoWrapperFactory.become(BlockingStateSqlDao.class);
+ final List<BlockingStateModelDao> modelsOnDisk = sqlDao.getBlockingAll(blockableId, context);
+ final List<BlockingStateModelDao> allModels = addBlockingStatesNotOnDisk(blockableId, modelsOnDisk, sqlDao, context);
+ return new ArrayList<BlockingState>(Collections2.transform(allModels, new Function<BlockingStateModelDao, BlockingState>() {
@Override
public BlockingState apply(@Nullable final BlockingStateModelDao src) {
return BlockingStateModelDao.toBlockingState(src);
@@ -119,8 +166,10 @@ public class DefaultBlockingStateDao implements BlockingStateDao {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<BlockingState>>() {
@Override
public List<BlockingState> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
- final List<BlockingStateModelDao> models = entitySqlDaoWrapperFactory.become(BlockingStateSqlDao.class).getByAccountRecordId(context);
- return new ArrayList<BlockingState>(Collections2.transform(models, new Function<BlockingStateModelDao, BlockingState>() {
+ final BlockingStateSqlDao sqlDao = entitySqlDaoWrapperFactory.become(BlockingStateSqlDao.class);
+ final List<BlockingStateModelDao> modelsOnDisk = sqlDao.getByAccountRecordId(context);
+ final List<BlockingStateModelDao> allModels = addBlockingStatesNotOnDisk(null, modelsOnDisk, sqlDao, context);
+ return new ArrayList<BlockingState>(Collections2.transform(allModels, new Function<BlockingStateModelDao, BlockingState>() {
@Override
public BlockingState apply(@Nullable final BlockingStateModelDao src) {
return BlockingStateModelDao.toBlockingState(src);
@@ -145,19 +194,7 @@ public class DefaultBlockingStateDao implements BlockingStateDao {
allForBlockedItAndService.add(newBlockingStateModelDao);
// Re-order what should be the final list (allForBlockedItAndService is ordered by record_id in the SQL and we just added a new state)
- final List<BlockingStateModelDao> allForBlockedItAndServiceOrdered = Ordering.<BlockingStateModelDao>from(new Comparator<BlockingStateModelDao>() {
- @Override
- public int compare(final BlockingStateModelDao o1, final BlockingStateModelDao o2) {
- // effective_date column NOT NULL
- final int comparison = o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
- if (comparison == 0) {
- // Keep a stable ordering for ties
- return o1.getCreatedDate().compareTo(o2.getCreatedDate());
- } else {
- return comparison;
- }
- }
- }).immutableSortedCopy(allForBlockedItAndService);
+ final List<BlockingStateModelDao> allForBlockedItAndServiceOrdered = BLOCKING_STATE_MODEL_DAO_ORDERING.immutableSortedCopy(allForBlockedItAndService);
// Go through the (ordered) stream of blocking states for that blocked id and service and check
// if there is one or more blocking states for the same state following each others.
@@ -203,4 +240,74 @@ public class DefaultBlockingStateDao implements BlockingStateDao {
}
});
}
+
+ // Add blocking states for add-ons, which would be impacted by a future cancellation or change of their base plan
+ // See DefaultEntitlement#blockAddOnsIfRequired
+ private List<BlockingStateModelDao> addBlockingStatesNotOnDisk(@Nullable final UUID blockableId,
+ final List<BlockingStateModelDao> blockingStatesOnDisk,
+ final BlockingStateSqlDao sqlDao,
+ final InternalTenantContext context) {
+ final Collection<BlockingStateModelDao> blockingStatesOnDiskCopy = new LinkedList<BlockingStateModelDao>(blockingStatesOnDisk);
+
+ // Find all base entitlements that we care about (for which we want to find future cancelled add-ons)
+ final Iterable<SubscriptionBase> baseSubscriptionsToConsider;
+ try {
+ if (blockableId == null) {
+ // We're coming from getBlockingAllForAccountRecordId
+ final Iterable<SubscriptionBase> subscriptions = Iterables.<SubscriptionBase>concat(subscriptionInternalApi.getSubscriptionsForAccount(context).values());
+ baseSubscriptionsToConsider = Iterables.<SubscriptionBase>filter(subscriptions,
+ new Predicate<SubscriptionBase>() {
+ @Override
+ public boolean apply(final SubscriptionBase input) {
+ return ProductCategory.BASE.equals(input.getCategory()) &&
+ !EntitlementState.CANCELLED.equals(input.getState());
+ }
+ });
+ } else {
+ // We're coming from getBlockingHistoryForService / getBlockingAll, but we don't know the blocking type
+ final SubscriptionBase addOnSubscription;
+ try {
+ addOnSubscription = subscriptionInternalApi.getSubscriptionFromId(blockableId, context);
+ } catch (SubscriptionBaseApiException ignored) {
+ // blockable id points to an account or bundle, in which case there are no extra blocking states to add
+ return blockingStatesOnDisk;
+ }
+
+ // blockable id points to a subscription, but make sure it's an add-on
+ if (ProductCategory.ADD_ON.equals(addOnSubscription.getCategory())) {
+ final SubscriptionBase baseSubscription = subscriptionInternalApi.getBaseSubscription(addOnSubscription.getBundleId(), context);
+ baseSubscriptionsToConsider = ImmutableList.<SubscriptionBase>of(baseSubscription);
+ } else {
+ // blockable id points to a base or standalone subscription, there is nothing to do
+ return blockingStatesOnDisk;
+ }
+ }
+ } catch (SubscriptionBaseApiException e) {
+ log.error("Error retrieving subscriptions for account record id " + context.getAccountRecordId(), e);
+ throw new RuntimeException(e);
+ }
+
+ final DateTime now = clock.getUTCNow();
+ for (final SubscriptionBase subscriptionBase : baseSubscriptionsToConsider) {
+ final Collection<BlockingState> blockingStates;
+ try {
+ blockingStates = entitlementUtils.computeFutureBlockingStatesForAssociatedAddons(sqlDao.getBlockingHistoryForService(subscriptionBase.getId(), EntitlementService.ENTITLEMENT_SERVICE_NAME, context),
+ subscriptionBase,
+ now,
+ context);
+ } catch (EntitlementApiException e) {
+ log.error("Error computing blocking states for addons for account record id " + context.getAccountRecordId(), e);
+ throw new RuntimeException(e);
+ }
+
+ // Inject the extra blocking states into the stream
+ for (final BlockingState blockingState : blockingStates) {
+ final BlockingStateModelDao blockingStateModelDao = new BlockingStateModelDao(blockingState, now, now);
+ blockingStatesOnDiskCopy.add(blockingStateModelDao);
+ }
+ }
+
+ // Return the sorted list
+ return BLOCKING_STATE_MODEL_DAO_ORDERING.immutableSortedCopy(blockingStatesOnDiskCopy);
+ }
}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/DefaultEntitlementService.java b/entitlement/src/main/java/com/ning/billing/entitlement/DefaultEntitlementService.java
index 5962bd1..ee87f96 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/DefaultEntitlementService.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/DefaultEntitlementService.java
@@ -16,10 +16,153 @@
package com.ning.billing.entitlement;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.ObjectType;
+import com.ning.billing.bus.api.BusEvent;
+import com.ning.billing.bus.api.PersistentBus;
+import com.ning.billing.bus.api.PersistentBus.EventBusException;
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.clock.Clock;
+import com.ning.billing.entitlement.api.DefaultBlockingTransitionInternalEvent;
+import com.ning.billing.entitlement.api.DefaultEntitlement;
+import com.ning.billing.entitlement.api.Entitlement;
+import com.ning.billing.entitlement.api.EntitlementApi;
+import com.ning.billing.entitlement.api.EntitlementApiException;
+import com.ning.billing.entitlement.engine.core.BlockingTransitionNotificationKey;
+import com.ning.billing.entitlement.engine.core.EntitlementNotificationKey;
+import com.ning.billing.entitlement.engine.core.EntitlementNotificationKeyAction;
+import com.ning.billing.lifecycle.LifecycleHandlerType;
+import com.ning.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
+import com.ning.billing.notificationq.api.NotificationEvent;
+import com.ning.billing.notificationq.api.NotificationQueue;
+import com.ning.billing.notificationq.api.NotificationQueueService;
+import com.ning.billing.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import com.ning.billing.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists;
+import com.ning.billing.notificationq.api.NotificationQueueService.NotificationQueueHandler;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.InternalCallContextFactory;
+import com.ning.billing.util.callcontext.UserType;
+import com.ning.billing.util.dao.NonEntityDao;
+
+import com.google.inject.Inject;
+
public class DefaultEntitlementService implements EntitlementService {
+ public static final String NOTIFICATION_QUEUE_NAME = "entitlement-events";
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultEntitlementService.class);
+
+ private final EntitlementApi entitlementApi;
+ private final NonEntityDao nonEntityDao;
+ private final Clock clock;
+ private final PersistentBus eventBus;
+ private final NotificationQueueService notificationQueueService;
+ private final InternalCallContextFactory internalCallContextFactory;
+
+ private NotificationQueue entitlementEventQueue;
+
+ @Inject
+ public DefaultEntitlementService(final EntitlementApi entitlementApi,
+ final NonEntityDao nonEntityDao,
+ final Clock clock,
+ final PersistentBus eventBus,
+ final NotificationQueueService notificationQueueService,
+ final InternalCallContextFactory internalCallContextFactory) {
+ this.entitlementApi = entitlementApi;
+ this.nonEntityDao = nonEntityDao;
+ this.clock = clock;
+ this.eventBus = eventBus;
+ this.notificationQueueService = notificationQueueService;
+ this.internalCallContextFactory = internalCallContextFactory;
+ }
+
@Override
public String getName() {
return EntitlementService.ENTITLEMENT_SERVICE_NAME;
}
-}
+
+ @LifecycleHandlerType(LifecycleLevel.INIT_SERVICE)
+ public void initialize() {
+ try {
+ final NotificationQueueHandler queueHandler = new NotificationQueueHandler() {
+ @Override
+ public void handleReadyNotification(final NotificationEvent inputKey, final DateTime eventDateTime, final UUID fromNotificationQueueUserToken, final Long accountRecordId, final Long tenantRecordId) {
+ final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "EntitlementQueue", CallOrigin.INTERNAL, UserType.SYSTEM, fromNotificationQueueUserToken);
+
+ if (inputKey instanceof EntitlementNotificationKey) {
+ final UUID tenantId = nonEntityDao.retrieveIdFromObject(tenantRecordId, ObjectType.TENANT);
+ processEntitlementNotification((EntitlementNotificationKey) inputKey, tenantId, internalCallContext);
+ } else if (inputKey instanceof BlockingTransitionNotificationKey) {
+ processBlockingNotification((BlockingTransitionNotificationKey) inputKey, internalCallContext);
+ } else if (inputKey != null) {
+ log.error("Entitlement service received an unexpected event type {}" + inputKey.getClass());
+ } else {
+ log.error("Entitlement service received an unexpected null event");
+ }
+ }
+ };
+
+ entitlementEventQueue = notificationQueueService.createNotificationQueue(ENTITLEMENT_SERVICE_NAME,
+ NOTIFICATION_QUEUE_NAME,
+ queueHandler);
+ } catch (final NotificationQueueAlreadyExists e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void processEntitlementNotification(final EntitlementNotificationKey key, final UUID tenantId, final InternalCallContext internalCallContext) {
+ final Entitlement entitlement;
+ try {
+ entitlement = entitlementApi.getEntitlementForId(key.getEntitlementId(), internalCallContext.toTenantContext(tenantId));
+ } catch (final EntitlementApiException e) {
+ log.error("Error retrieving entitlement for id " + key.getEntitlementId(), e);
+ return;
+ }
+
+ if (!(entitlement instanceof DefaultEntitlement)) {
+ log.error("Entitlement service received an unexpected entitlement class type {}" + entitlement.getClass().getName());
+ return;
+ }
+
+ final EntitlementNotificationKeyAction entitlementNotificationKeyAction = key.getEntitlementNotificationKeyAction();
+ if (EntitlementNotificationKeyAction.CHANGE.equals(entitlementNotificationKeyAction) ||
+ EntitlementNotificationKeyAction.CANCEL.equals(entitlementNotificationKeyAction)) {
+ try {
+ ((DefaultEntitlement) entitlement).blockAddOnsIfRequired(key.getEffectiveDate(), internalCallContext.toTenantContext(tenantId), internalCallContext);
+ } catch (EntitlementApiException e) {
+ log.error("Error processing event for entitlement {}" + entitlement.getId(), e);
+ }
+ }
+ }
+
+ private void processBlockingNotification(final BlockingTransitionNotificationKey key, final InternalCallContext internalCallContext) {
+ final BusEvent event = new DefaultBlockingTransitionInternalEvent(key.getBlockableId(), key.getBlockingType(),
+ key.isTransitionedToBlockedBilling(), key.isTransitionedToUnblockedBilling(),
+ key.isTransitionedToBlockedEntitlement(), key.isTransitionToUnblockedEntitlement(),
+ internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId(), internalCallContext.getUserToken());
+
+ try {
+ eventBus.post(event);
+ } catch (EventBusException e) {
+ log.warn("Failed to post event {}", e);
+ }
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.START_SERVICE)
+ public void start() {
+ entitlementEventQueue.startQueue();
+ }
+
+ @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE)
+ public void stop() throws NoSuchNotificationQueue {
+ if (entitlementEventQueue != null) {
+ entitlementEventQueue.stopQueue();
+ notificationQueueService.deleteNotificationQueue(entitlementEventQueue.getServiceName(), entitlementEventQueue.getQueueName());
+ }
+ }
+}
\ No newline at end of file
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/BlockingTransitionNotificationKey.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/BlockingTransitionNotificationKey.java
new file mode 100644
index 0000000..441877e
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/BlockingTransitionNotificationKey.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.engine.core;
+
+import java.util.UUID;
+
+import com.ning.billing.entitlement.api.BlockingStateType;
+import com.ning.billing.notificationq.api.NotificationEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class BlockingTransitionNotificationKey implements NotificationEvent {
+
+ private final UUID blockableId;
+ private final BlockingStateType blockingType;
+ private final Boolean isTransitionToBlockedBilling;
+ private final Boolean isTransitionToUnblockedBilling;
+ private final Boolean isTransitionToBlockedEntitlement;
+ private final Boolean isTransitionToUnblockedEntitlement;
+
+ @JsonCreator
+ public BlockingTransitionNotificationKey(@JsonProperty("blockableId") final UUID blockableId,
+ @JsonProperty("type") final BlockingStateType blockingType,
+ @JsonProperty("isTransitionToBlockedBilling") final Boolean isTransitionToBlockedBilling,
+ @JsonProperty("isTransitionToUnblockedBilling") final Boolean isTransitionToUnblockedBilling,
+ @JsonProperty("isTransitionToBlockedEntitlement") final Boolean isTransitionToBlockedEntitlement,
+ @JsonProperty("isTransitionToUnblockedEntitlement") final Boolean isTransitionToUnblockedEntitlement) {
+
+ this.blockableId = blockableId;
+ this.blockingType = blockingType;
+ this.isTransitionToBlockedBilling = isTransitionToBlockedBilling;
+ this.isTransitionToUnblockedBilling = isTransitionToUnblockedBilling;
+ this.isTransitionToBlockedEntitlement = isTransitionToBlockedEntitlement;
+ this.isTransitionToUnblockedEntitlement = isTransitionToUnblockedEntitlement;
+ }
+
+ public UUID getBlockableId() {
+ return blockableId;
+ }
+
+ public BlockingStateType getBlockingType() {
+ return blockingType;
+ }
+
+ @JsonProperty("isTransitionToBlockedBilling")
+ public Boolean isTransitionedToBlockedBilling() {
+ return isTransitionToBlockedBilling;
+ }
+
+ @JsonProperty("isTransitionToUnblockedBilling")
+ public Boolean isTransitionedToUnblockedBilling() {
+ return isTransitionToUnblockedBilling;
+ }
+
+ @JsonProperty("isTransitionToBlockedEntitlement")
+ public Boolean isTransitionedToBlockedEntitlement() {
+ return isTransitionToBlockedEntitlement;
+ }
+
+ @JsonProperty("isTransitionToUnblockedEntitlement")
+ public Boolean isTransitionToUnblockedEntitlement() {
+ return isTransitionToUnblockedEntitlement;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("BlockingTransitionNotificationKey{");
+ sb.append("blockableId=").append(blockableId);
+ sb.append(", blockingType=").append(blockingType);
+ sb.append(", isTransitionToBlockedBilling=").append(isTransitionToBlockedBilling);
+ sb.append(", isTransitionToUnblockedBilling=").append(isTransitionToUnblockedBilling);
+ sb.append(", isTransitionToBlockedEntitlement=").append(isTransitionToBlockedEntitlement);
+ sb.append(", isTransitionToUnblockedEntitlement=").append(isTransitionToUnblockedEntitlement);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final BlockingTransitionNotificationKey that = (BlockingTransitionNotificationKey) o;
+
+ if (blockableId != null ? !blockableId.equals(that.blockableId) : that.blockableId != null) {
+ return false;
+ }
+ if (blockingType != that.blockingType) {
+ return false;
+ }
+ if (isTransitionToBlockedBilling != null ? !isTransitionToBlockedBilling.equals(that.isTransitionToBlockedBilling) : that.isTransitionToBlockedBilling != null) {
+ return false;
+ }
+ if (isTransitionToBlockedEntitlement != null ? !isTransitionToBlockedEntitlement.equals(that.isTransitionToBlockedEntitlement) : that.isTransitionToBlockedEntitlement != null) {
+ return false;
+ }
+ if (isTransitionToUnblockedBilling != null ? !isTransitionToUnblockedBilling.equals(that.isTransitionToUnblockedBilling) : that.isTransitionToUnblockedBilling != null) {
+ return false;
+ }
+ if (isTransitionToUnblockedEntitlement != null ? !isTransitionToUnblockedEntitlement.equals(that.isTransitionToUnblockedEntitlement) : that.isTransitionToUnblockedEntitlement != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = blockableId != null ? blockableId.hashCode() : 0;
+ result = 31 * result + (blockingType != null ? blockingType.hashCode() : 0);
+ result = 31 * result + (isTransitionToBlockedBilling != null ? isTransitionToBlockedBilling.hashCode() : 0);
+ result = 31 * result + (isTransitionToUnblockedBilling != null ? isTransitionToUnblockedBilling.hashCode() : 0);
+ result = 31 * result + (isTransitionToBlockedEntitlement != null ? isTransitionToBlockedEntitlement.hashCode() : 0);
+ result = 31 * result + (isTransitionToUnblockedEntitlement != null ? isTransitionToUnblockedEntitlement.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementNotificationKey.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementNotificationKey.java
new file mode 100644
index 0000000..b9da6a6
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementNotificationKey.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.engine.core;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.notificationq.api.NotificationEvent;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class EntitlementNotificationKey implements NotificationEvent {
+
+ private final UUID entitlementId;
+ private final EntitlementNotificationKeyAction entitlementNotificationKeyAction;
+ private final DateTime effectiveDate;
+
+ @JsonCreator
+ public EntitlementNotificationKey(@JsonProperty("entitlementId") final UUID entitlementId,
+ @JsonProperty("entitlementNotificationKeyAction") final EntitlementNotificationKeyAction entitlementNotificationKeyAction,
+ @JsonProperty("effectiveDate") final DateTime effectiveDate) {
+ this.entitlementId = entitlementId;
+ this.entitlementNotificationKeyAction = entitlementNotificationKeyAction;
+ this.effectiveDate = effectiveDate;
+ }
+
+ public UUID getEntitlementId() {
+ return entitlementId;
+ }
+
+ public EntitlementNotificationKeyAction getEntitlementNotificationKeyAction() {
+ return entitlementNotificationKeyAction;
+ }
+
+ public DateTime getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("EntitlementNotificationKey{");
+ sb.append("entitlementId=").append(entitlementId);
+ sb.append(", entitlementNotificationKeyAction=").append(entitlementNotificationKeyAction);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final EntitlementNotificationKey that = (EntitlementNotificationKey) o;
+
+ if (entitlementId != null ? !entitlementId.equals(that.entitlementId) : that.entitlementId != null) {
+ return false;
+ }
+ if (entitlementNotificationKeyAction != that.entitlementNotificationKeyAction) {
+ return false;
+ }
+ if (effectiveDate != null ? effectiveDate.compareTo(that.effectiveDate) != 0 : that.effectiveDate != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = entitlementId != null ? entitlementId.hashCode() : 0;
+ result = 31 * result + (entitlementNotificationKeyAction != null ? entitlementNotificationKeyAction.hashCode() : 0);
+ result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementNotificationKeyAction.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementNotificationKeyAction.java
new file mode 100644
index 0000000..446cfc9
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementNotificationKeyAction.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.engine.core;
+
+public enum EntitlementNotificationKeyAction {
+ CANCEL,
+ CHANGE
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementUtils.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementUtils.java
new file mode 100644
index 0000000..22b4f3b
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EntitlementUtils.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.engine.core;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.bus.api.BusEvent;
+import com.ning.billing.bus.api.PersistentBus;
+import com.ning.billing.bus.api.PersistentBus.EventBusException;
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.Product;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.clock.Clock;
+import com.ning.billing.entitlement.DefaultEntitlementService;
+import com.ning.billing.entitlement.EntitlementService;
+import com.ning.billing.entitlement.api.BlockingApiException;
+import com.ning.billing.entitlement.api.BlockingState;
+import com.ning.billing.entitlement.api.BlockingStateType;
+import com.ning.billing.entitlement.api.DefaultBlockingTransitionInternalEvent;
+import com.ning.billing.entitlement.api.DefaultEntitlementApi;
+import com.ning.billing.entitlement.api.Entitlement.EntitlementState;
+import com.ning.billing.entitlement.api.EntitlementApiException;
+import com.ning.billing.entitlement.block.BlockingChecker;
+import com.ning.billing.entitlement.block.BlockingChecker.BlockingAggregator;
+import com.ning.billing.entitlement.dao.BlockingStateDao;
+import com.ning.billing.entitlement.dao.BlockingStateModelDao;
+import com.ning.billing.junction.DefaultBlockingState;
+import com.ning.billing.notificationq.api.NotificationEvent;
+import com.ning.billing.notificationq.api.NotificationQueue;
+import com.ning.billing.notificationq.api.NotificationQueueService;
+import com.ning.billing.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
+import com.ning.billing.subscription.api.SubscriptionBase;
+import com.ning.billing.subscription.api.SubscriptionBaseInternalApi;
+import com.ning.billing.subscription.api.SubscriptionBaseTransitionType;
+import com.ning.billing.subscription.api.user.SubscriptionBaseTransition;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+public class EntitlementUtils {
+
+ private static final Logger log = LoggerFactory.getLogger(EntitlementUtils.class);
+
+ private final SubscriptionBaseInternalApi subscriptionInternalApi;
+ private final BlockingStateDao dao;
+ private final BlockingChecker blockingChecker;
+ private final PersistentBus eventBus;
+ private final Clock clock;
+ protected final NotificationQueueService notificationQueueService;
+
+ @Inject
+ public EntitlementUtils(final SubscriptionBaseInternalApi subscriptionInternalApi,
+ final BlockingStateDao dao, final BlockingChecker blockingChecker,
+ final PersistentBus eventBus, final Clock clock,
+ final NotificationQueueService notificationQueueService) {
+ this.subscriptionInternalApi = subscriptionInternalApi;
+ this.dao = dao;
+ this.blockingChecker = blockingChecker;
+ this.eventBus = eventBus;
+ this.clock = clock;
+ this.notificationQueueService = notificationQueueService;
+ }
+
+ /**
+ * Compute future blocking states for addons associated to a base subscription following a change or a cancellation
+ * on that base subscription
+ * <p/>
+ * This is only used in the "read" path, to add events that are not on disk.
+ * See same logic in DefaultSubscriptionDao#buildBundleSubscriptions
+ *
+ * @param blockingStates existing entitlement blocking states for that base subscription
+ * @param subscriptionBase base subscription, reflecting the latest state (cancellation or change applied)
+ * @param now present reference time (to avoid timing issues in DefaultEntitlement#blockAddOnsIfRequired)
+ * @param internalTenantContext context @return the blocking states for the add-ons
+ * @throws EntitlementApiException
+ */
+ public Collection<BlockingState> computeFutureBlockingStatesForAssociatedAddons(final Iterable<BlockingStateModelDao> blockingStates,
+ final SubscriptionBase subscriptionBase,
+ final DateTime now,
+ final InternalTenantContext internalTenantContext) throws EntitlementApiException {
+ if (!ProductCategory.BASE.equals(subscriptionBase.getCategory())) {
+ // Only base subscriptions have add-ons
+ return ImmutableList.<BlockingState>of();
+ }
+
+ // We need to find the first "trigger" transition, from which we will create the add-ons cancellation events.
+ // This can either be a future entitlement cancel...
+ final BlockingStateModelDao futureEntitlementCancelEvent = Iterables.<BlockingStateModelDao>tryFind(blockingStates,
+ new Predicate<BlockingStateModelDao>() {
+ @Override
+ public boolean apply(final BlockingStateModelDao input) {
+ // Look at future cancellations only
+ return input.getEffectiveDate().isAfter(now) &&
+ EntitlementService.ENTITLEMENT_SERVICE_NAME.equals(input.getService()) &&
+ DefaultEntitlementApi.ENT_STATE_CANCELLED.equals(input.getState());
+ }
+ }).orNull();
+
+ if (futureEntitlementCancelEvent != null) {
+ // Note that in theory we could always only look subscription base as we assume entitlement cancel means subscription base cancel
+ // but we want to use the effective date of the entitlement cancel event to create the add-on cancel event
+ return computeBlockingStatesForAssociatedAddons(subscriptionBase.getBundleId(), null, futureEntitlementCancelEvent.getEffectiveDate(), internalTenantContext);
+ } else {
+ // ...or a subscription change (i.e. a change plan where the new plan has an impact on the existing add-on).
+ // We need to go back to subscription base as entitlement doesn't know about these
+ final SubscriptionBaseTransition futureSubscriptionBaseChangeEvent = Iterables.<SubscriptionBaseTransition>tryFind(subscriptionBase.getAllTransitions(),
+ new Predicate<SubscriptionBaseTransition>() {
+ @Override
+ public boolean apply(final SubscriptionBaseTransition input) {
+ // Look at future changes only
+ return input.getEffectiveTransitionTime().isAfter(now) &&
+ (SubscriptionBaseTransitionType.CHANGE.equals(input.getTransitionType()) ||
+ // This should never happen, as we should always have an entitlement cancel event
+ // (see above), but add it just in case...
+ SubscriptionBaseTransitionType.CANCEL.equals(input.getTransitionType()));
+ }
+ }).orNull();
+ if (futureSubscriptionBaseChangeEvent == null) {
+ // Nothing to do
+ return ImmutableList.<BlockingState>of();
+ }
+
+ final Plan nextPlan = futureSubscriptionBaseChangeEvent.getNextPlan();
+ final Product product = nextPlan == null ? futureSubscriptionBaseChangeEvent.getPreviousPlan().getProduct() : nextPlan.getProduct();
+ return computeBlockingStatesForAssociatedAddons(subscriptionBase.getBundleId(), product, futureSubscriptionBaseChangeEvent.getEffectiveTransitionTime(), internalTenantContext);
+ }
+ }
+
+ // "write" path (when the cancellation/change is effective)
+ public Collection<BlockingState> computeBlockingStatesForAssociatedAddons(final SubscriptionBase subscriptionBase, final DateTime effectiveDate, final InternalTenantContext internalTenantContext) {
+ return computeBlockingStatesForAssociatedAddons(subscriptionBase.getBundleId(), EntitlementState.CANCELLED.equals(subscriptionBase.getState()) ? null : subscriptionBase.getLastActiveProduct(), effectiveDate, internalTenantContext);
+ }
+
+ private Collection<BlockingState> computeBlockingStatesForAssociatedAddons(final UUID bundleId,
+ @Nullable final Product baseTransitionTriggerNextProduct,
+ final DateTime baseTransitionTriggerEffectiveTransitionTime,
+ final InternalTenantContext internalTenantContext) {
+ final Collection<String> includedAddonsForProduct;
+ final Collection<String> availableAddonsForProduct;
+ if (baseTransitionTriggerNextProduct == null) {
+ includedAddonsForProduct = ImmutableList.<String>of();
+ availableAddonsForProduct = ImmutableList.<String>of();
+ } else {
+ includedAddonsForProduct = Collections2.<Product, String>transform(ImmutableSet.<Product>copyOf(baseTransitionTriggerNextProduct.getIncluded()),
+ new Function<Product, String>() {
+ @Override
+ public String apply(final Product product) {
+ return product.getName();
+ }
+ });
+
+ availableAddonsForProduct = Collections2.<Product, String>transform(ImmutableSet.<Product>copyOf(baseTransitionTriggerNextProduct.getAvailable()),
+ new Function<Product, String>() {
+ @Override
+ public String apply(final Product product) {
+ return product.getName();
+ }
+ });
+ }
+
+ // Retrieve all add-ons to block for that base subscription
+ final List<SubscriptionBase> subscriptionsForBundle = subscriptionInternalApi.getSubscriptionsForBundle(bundleId, internalTenantContext);
+ final Collection<SubscriptionBase> futureBlockedAddons = Collections2.<SubscriptionBase>filter(subscriptionsForBundle,
+ new Predicate<SubscriptionBase>() {
+ @Override
+ public boolean apply(final SubscriptionBase subscription) {
+ return ProductCategory.ADD_ON.equals(subscription.getCategory()) &&
+ (
+ // Base entitlement cancelled, cancel all add-ons
+ // We don't check if the associated entitlement had already blocked here
+ // but the dao should eventually do the right thing (it won't insert duplicated
+ // blocking events)
+ baseTransitionTriggerNextProduct == null ||
+ (
+ // Change plan - check which add-ons to cancel
+ includedAddonsForProduct.contains(subscription.getLastActivePlan().getProduct().getName()) ||
+ !availableAddonsForProduct.contains(subscription.getLastActivePlan().getProduct().getName())
+ )
+ );
+ }
+ });
+ return Collections2.<SubscriptionBase, BlockingState>transform(futureBlockedAddons,
+ new Function<SubscriptionBase, BlockingState>() {
+ @Override
+ public BlockingState apply(final SubscriptionBase input) {
+ return new DefaultBlockingState(input.getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_CANCELLED, EntitlementService.ENTITLEMENT_SERVICE_NAME, true, true, false, baseTransitionTriggerEffectiveTransitionTime);
+ }
+ });
+ }
+
+ /**
+ * Wrapper around BlockingStateDao#setBlockingState which will send an event on the bus if needed
+ *
+ * @param state new state to store
+ * @param context call context
+ */
+ public void setBlockingStateAndPostBlockingTransitionEvent(final BlockingState state, final InternalCallContext context) {
+ final BlockingAggregator previousState = getBlockingStateFor(state.getBlockedId(), state.getType(), context);
+
+ dao.setBlockingState(state, clock, context);
+
+ final BlockingAggregator currentState = getBlockingStateFor(state.getBlockedId(), state.getType(), context);
+ if (previousState != null && currentState != null) {
+ postBlockingTransitionEvent(state.getEffectiveDate(), state.getBlockedId(), state.getType(), previousState, currentState, context);
+ }
+ }
+
+ private BlockingAggregator getBlockingStateFor(final UUID blockableId, final BlockingStateType type, final InternalCallContext context) {
+ try {
+ return blockingChecker.getBlockedStatus(blockableId, type, context);
+ } catch (BlockingApiException e) {
+ log.warn("Failed to retrieve blocking state for {} {}", blockableId, type);
+ return null;
+ }
+ }
+
+ private void postBlockingTransitionEvent(final DateTime effectiveDate, final UUID blockableId, final BlockingStateType type,
+ final BlockingAggregator previousState, final BlockingAggregator currentState,
+ final InternalCallContext context) {
+ final boolean isTransitionToBlockedBilling = !previousState.isBlockBilling() && currentState.isBlockBilling();
+ final boolean isTransitionToUnblockedBilling = previousState.isBlockBilling() && !currentState.isBlockBilling();
+
+ final boolean isTransitionToBlockedEntitlement = !previousState.isBlockEntitlement() && currentState.isBlockEntitlement();
+ final boolean isTransitionToUnblockedEntitlement = previousState.isBlockEntitlement() && !currentState.isBlockEntitlement();
+
+ if (effectiveDate.compareTo(clock.getUTCNow()) > 0) {
+ // Add notification entry to send the bus event at the effective date
+ final NotificationEvent notificationEvent = new BlockingTransitionNotificationKey(blockableId, type,
+ isTransitionToBlockedBilling, isTransitionToUnblockedBilling,
+ isTransitionToBlockedEntitlement, isTransitionToUnblockedEntitlement);
+ recordFutureNotification(effectiveDate, notificationEvent, context);
+ } else {
+ // TODO Do we want to send a DefaultEffectiveEntitlementEvent for entitlement specific blocking states?
+ final BusEvent event = new DefaultBlockingTransitionInternalEvent(blockableId, type,
+ isTransitionToBlockedBilling, isTransitionToUnblockedBilling,
+ isTransitionToBlockedEntitlement, isTransitionToUnblockedEntitlement,
+ context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+
+ postBusEvent(event);
+ }
+ }
+
+ private void postBusEvent(final BusEvent event) {
+ try {
+ // TODO STEPH Ideally we would like to post from transaction when we inserted the new blocking state, but new state would have to be recalculated from transaction which is
+ // difficult without the help of BlockingChecker -- which itself relies on dao. Other alternative is duplicating the logic, or refactoring the DAO to export higher level api.
+ eventBus.post(event);
+ } catch (EventBusException e) {
+ log.warn("Failed to post event {}", e);
+ }
+ }
+
+ private void recordFutureNotification(final DateTime effectiveDate,
+ final NotificationEvent notificationEvent,
+ final InternalCallContext context) {
+ try {
+ final NotificationQueue subscriptionEventQueue = notificationQueueService.getNotificationQueue(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
+ DefaultEntitlementService.NOTIFICATION_QUEUE_NAME);
+ subscriptionEventQueue.recordFutureNotification(effectiveDate, notificationEvent, context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId());
+ } catch (NoSuchNotificationQueue e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/glue/DefaultEntitlementModule.java b/entitlement/src/main/java/com/ning/billing/entitlement/glue/DefaultEntitlementModule.java
index 776b635..c7d9e26 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/glue/DefaultEntitlementModule.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/glue/DefaultEntitlementModule.java
@@ -29,6 +29,7 @@ import com.ning.billing.entitlement.block.BlockingChecker;
import com.ning.billing.entitlement.block.DefaultBlockingChecker;
import com.ning.billing.entitlement.dao.BlockingStateDao;
import com.ning.billing.entitlement.dao.DefaultBlockingStateDao;
+import com.ning.billing.entitlement.engine.core.EntitlementUtils;
import com.ning.billing.glue.EntitlementModule;
import com.ning.billing.junction.BlockingInternalApi;
@@ -48,6 +49,7 @@ public class DefaultEntitlementModule extends AbstractModule implements Entitlem
installSubscriptionApi();
installBlockingChecker();
bind(EntitlementService.class).to(DefaultEntitlementService.class).asEagerSingleton();
+ bind(EntitlementUtils.class).asEagerSingleton();
}
@Override
@@ -60,7 +62,6 @@ public class DefaultEntitlementModule extends AbstractModule implements Entitlem
bind(BlockingInternalApi.class).to(DefaultInternalBlockingApi.class).asEagerSingleton();
}
-
@Override
public void installEntitlementApi() {
bind(EntitlementApi.class).to(DefaultEntitlementApi.class).asEagerSingleton();
@@ -74,6 +75,4 @@ public class DefaultEntitlementModule extends AbstractModule implements Entitlem
public void installBlockingChecker() {
bind(BlockingChecker.class).to(DefaultBlockingChecker.class).asEagerSingleton();
}
-
-
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/dao/TestDefaultBlockingStateDao.java b/entitlement/src/test/java/com/ning/billing/entitlement/dao/TestDefaultBlockingStateDao.java
index 31a87a2..ecca804 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/dao/TestDefaultBlockingStateDao.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/dao/TestDefaultBlockingStateDao.java
@@ -31,6 +31,27 @@ import com.ning.billing.junction.DefaultBlockingState;
public class TestDefaultBlockingStateDao extends EntitlementTestSuiteWithEmbeddedDB {
+ @Test(groups = "slow", description = "Verify we don't insert extra add-on events")
+ public void testUnnecessaryEventsAreNotAdded() throws Exception {
+ // This is a simple smoke test at the dao level only to make sure we do sane
+ // things in case there are no future add-on cancellation events to add in the stream.
+ // See TestEntitlementUtils for a more comprehensive test
+ final UUID blockableId = UUID.randomUUID();
+ final BlockingStateType type = BlockingStateType.SUBSCRIPTION;
+ final String state = "state";
+ final String service = "service";
+
+ // Verify initial state
+ Assert.assertEquals(blockingStateDao.getBlockingAll(blockableId, internalCallContext).size(), 0);
+
+ // Set a state
+ final DateTime stateDateTime = new DateTime(2013, 5, 6, 10, 11, 12, DateTimeZone.UTC);
+ final BlockingState blockingState = new DefaultBlockingState(blockableId, type, state, service, false, false, false, stateDateTime);
+ blockingStateDao.setBlockingState(blockingState, clock, internalCallContext);
+
+ Assert.assertEquals(blockingStateDao.getBlockingAll(blockableId, internalCallContext).size(), 1);
+ }
+
// See https://github.com/killbill/killbill/issues/111
@Test(groups = "slow", description = "Verify we don't insert duplicate blocking states")
public void testSetBlockingState() throws Exception {
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/engine/core/TestEntitlementUtils.java b/entitlement/src/test/java/com/ning/billing/entitlement/engine/core/TestEntitlementUtils.java
new file mode 100644
index 0000000..feb040c
--- /dev/null
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/engine/core/TestEntitlementUtils.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.engine.core;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.api.TestApiListener.NextEvent;
+import com.ning.billing.catalog.api.BillingActionPolicy;
+import com.ning.billing.catalog.api.BillingPeriod;
+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.EntitlementService;
+import com.ning.billing.entitlement.EntitlementTestSuiteWithEmbeddedDB;
+import com.ning.billing.entitlement.api.BlockingState;
+import com.ning.billing.entitlement.api.BlockingStateType;
+import com.ning.billing.entitlement.api.DefaultEntitlement;
+import com.ning.billing.entitlement.api.DefaultEntitlementApi;
+import com.ning.billing.entitlement.api.Entitlement;
+import com.ning.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
+import com.ning.billing.entitlement.api.EntitlementApiException;
+import com.ning.billing.entitlement.dao.BlockingStateSqlDao;
+
+import static org.testng.Assert.assertTrue;
+
+public class TestEntitlementUtils extends EntitlementTestSuiteWithEmbeddedDB {
+
+ private BlockingStateSqlDao sqlDao;
+ private DefaultEntitlement baseEntitlement;
+ private DefaultEntitlement addOnEntitlement;
+ // Dates for the base plan only
+ private DateTime baseEffectiveEOTCancellationOrChangeDateTime;
+ private LocalDate baseEffectiveCancellationOrChangeDate;
+
+ @BeforeMethod(groups = "slow")
+ public void setUp() throws Exception {
+ sqlDao = dbi.onDemand(BlockingStateSqlDao.class);
+
+ final LocalDate initialDate = new LocalDate(2013, 8, 8);
+ clock.setDay(initialDate);
+ final Account account = accountApi.createAccount(getAccountData(7), callContext);
+
+ testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.CREATE);
+
+ // Create base entitlement
+ final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ baseEntitlement = (DefaultEntitlement) entitlementApi.createBaseEntitlement(account.getId(), baseSpec, account.getExternalKey(), initialDate, callContext);
+
+ // Add ADD_ON
+ final PlanPhaseSpecifier addOnSpec = new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ addOnEntitlement = (DefaultEntitlement) entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec, initialDate, callContext);
+
+ // Verify the initial state
+ checkFutureBlockingStatesToCancel(baseEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(addOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(baseEntitlement, addOnEntitlement, null);
+
+ testListener.pushExpectedEvents(NextEvent.PHASE, NextEvent.PHASE);
+ // Phase for the base plan is 2013/09/07 (30 days trial) but it's 2013/09/08 for the add-on (1 month discount)
+ clock.setDay(new LocalDate(2013, 9, 8));
+ assertTrue(testListener.isCompleted(DELAY));
+
+ // Note! Make sure to align CTD and cancellation/change effective time with the phase event effective time to avoid timing issues in comparisons
+ baseEffectiveEOTCancellationOrChangeDateTime = baseEntitlement.getSubscriptionBase().getAllTransitions().get(1).getEffectiveTransitionTime().plusMonths(1);
+ Assert.assertEquals(baseEffectiveEOTCancellationOrChangeDateTime.toLocalDate(), new LocalDate(2013, 10, 7));
+ baseEffectiveCancellationOrChangeDate = baseEffectiveEOTCancellationOrChangeDateTime.toLocalDate();
+ // Set manually since no invoice
+ subscriptionInternalApi.setChargedThroughDate(baseEntitlement.getId(), baseEffectiveEOTCancellationOrChangeDateTime, internalCallContext);
+ }
+
+ @Test(groups = "slow", description = "Verify add-ons blocking states are added for EOT cancellations")
+ public void testCancellationEOT() throws Exception {
+ // Cancel the base plan
+ final DefaultEntitlement cancelledBaseEntitlement = (DefaultEntitlement) baseEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.END_OF_TERM, BillingActionPolicy.END_OF_TERM, callContext);
+ // No blocking event (EOT)
+ assertTrue(testListener.isCompleted(DELAY));
+
+ // Verify we compute the right blocking states for the "read" path...
+ checkFutureBlockingStatesToCancel(addOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, addOnEntitlement, baseEffectiveEOTCancellationOrChangeDateTime);
+ // and for the "write" path (which will be exercised when the future notification kicks in).
+ // Note that no event are computed because the add-on is not cancelled yet
+ checkActualBlockingStatesToCancel(cancelledBaseEntitlement, addOnEntitlement, null);
+ // Verify also the blocking states DAO adds events not on disk
+ checkBlockingStatesDAO(baseEntitlement, addOnEntitlement, baseEffectiveCancellationOrChangeDate, true);
+
+ // Verify the notification kicks in
+ testListener.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.BLOCK);
+ clock.addDays(30);
+ assertTrue(testListener.isCompleted(DELAY));
+
+ // Refresh the state
+ final DefaultEntitlement cancelledAddOnEntitlement = (DefaultEntitlement) entitlementApi.getEntitlementForId(addOnEntitlement.getId(), callContext);
+
+ // Verify we compute the right blocking states for the "read" path...
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledAddOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, cancelledAddOnEntitlement, null);
+ // ...and for the "write" path (which has been exercised when the notification kicked in).
+ checkActualBlockingStatesToCancel(cancelledBaseEntitlement, cancelledAddOnEntitlement, baseEffectiveEOTCancellationOrChangeDateTime);
+ // Verify also the blocking states API doesn't add too many events (now on disk)
+ checkBlockingStatesDAO(cancelledBaseEntitlement, cancelledAddOnEntitlement, baseEffectiveCancellationOrChangeDate, true);
+ }
+
+ @Test(groups = "slow", description = "Verify add-ons blocking states are not impacted by IMM cancellations")
+ public void testCancellationIMM() throws Exception {
+ // A bit fragile? The blocking state check (checkBlockingStatesDAO) could be a bit off
+ final DateTime cancellationDateTime = clock.getUTCNow();
+ final LocalDate cancellationDate = clock.getUTCToday();
+
+ // Cancel the base plan
+ testListener.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.CANCEL, NextEvent.BLOCK);
+ final DefaultEntitlement cancelledBaseEntitlement = (DefaultEntitlement) baseEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.IMMEDIATE, callContext);
+ assertTrue(testListener.isCompleted(DELAY));
+
+ // Refresh the add-on state
+ final DefaultEntitlement cancelledAddOnEntitlement = (DefaultEntitlement) entitlementApi.getEntitlementForId(addOnEntitlement.getId(), callContext);
+
+ // Verify we compute the right blocking states for the "read" path...
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledAddOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, cancelledAddOnEntitlement, null);
+ // ...and for the "write" path (which has been exercised in the cancel call above).
+ checkActualBlockingStatesToCancel(cancelledBaseEntitlement, cancelledAddOnEntitlement, cancellationDateTime);
+ // Verify also the blocking states DAO doesn't add too many events (all on disk)
+ checkBlockingStatesDAO(cancelledBaseEntitlement, cancelledAddOnEntitlement, cancellationDate, true);
+
+ clock.addDays(30);
+ // No new event
+ assertTrue(testListener.isCompleted(DELAY));
+
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledAddOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, cancelledAddOnEntitlement, null);
+ checkActualBlockingStatesToCancel(cancelledBaseEntitlement, cancelledAddOnEntitlement, cancellationDateTime);
+ checkBlockingStatesDAO(cancelledBaseEntitlement, cancelledAddOnEntitlement, cancellationDate, true);
+ }
+
+ @Test(groups = "slow", description = "Verify add-ons blocking states are added for EOT change plans")
+ public void testChangePlanEOT() throws Exception {
+ // Change plan EOT to Assault-Rifle (Telescopic-Scope is included)
+ final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlanWithDate("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, new LocalDate(2013, 10, 7), callContext);
+ // No blocking event (EOT)
+ assertTrue(testListener.isCompleted(DELAY));
+
+ // Verify we compute the right blocking states for the "read" path...
+ checkFutureBlockingStatesToCancel(addOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(changedBaseEntitlement, addOnEntitlement, baseEffectiveEOTCancellationOrChangeDateTime);
+ // ...and for the "write" path (which will be exercised when the future notification kicks in).
+ // Note that no event are computed because the add-on is not cancelled yet
+ checkActualBlockingStatesToCancel(changedBaseEntitlement, addOnEntitlement, null);
+ // Verify also the blocking states DAO adds events not on disk
+ checkBlockingStatesDAO(changedBaseEntitlement, addOnEntitlement, baseEffectiveCancellationOrChangeDate, false);
+
+ // Verify the notification kicks in
+ testListener.pushExpectedEvent(NextEvent.BLOCK);
+ clock.addDays(30);
+ assertTrue(testListener.isCompleted(DELAY));
+
+ // Refresh the state
+ final DefaultEntitlement cancelledAddOnEntitlement = (DefaultEntitlement) entitlementApi.getEntitlementForId(addOnEntitlement.getId(), callContext);
+
+ // Verify we compute the right blocking states for the "read" path...
+ checkFutureBlockingStatesToCancel(changedBaseEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledAddOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(changedBaseEntitlement, cancelledAddOnEntitlement, null);
+ // ...and for the "write" path (which has been exercised when the notification kicked in).
+ checkActualBlockingStatesToCancel(changedBaseEntitlement, cancelledAddOnEntitlement, baseEffectiveEOTCancellationOrChangeDateTime);
+ // Verify also the blocking states API doesn't add too many events (now on disk)
+ checkBlockingStatesDAO(changedBaseEntitlement, cancelledAddOnEntitlement, baseEffectiveCancellationOrChangeDate, false);
+ }
+
+ @Test(groups = "slow", description = "Verify add-ons blocking states are added for IMM change plans")
+ public void testChangePlanIMM() throws Exception {
+ // A bit fragile? The blocking state check (checkBlockingStatesDAO) could be a bit off
+ final DateTime changeDateTime = clock.getUTCNow();
+ final LocalDate changeDate = clock.getUTCToday();
+
+ // Change plan IMM (upgrade) to Assault-Rifle (Telescopic-Scope is included)
+ testListener.pushExpectedEvents(NextEvent.CANCEL, NextEvent.BLOCK);
+ final DefaultEntitlement changedBaseEntitlement = (DefaultEntitlement) baseEntitlement.changePlan("Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, callContext);
+ assertTrue(testListener.isCompleted(DELAY));
+
+ // Refresh the add-on state
+ final DefaultEntitlement cancelledAddOnEntitlement = (DefaultEntitlement) entitlementApi.getEntitlementForId(addOnEntitlement.getId(), callContext);
+
+ // Verify we compute the right blocking states for the "read" path...
+ checkFutureBlockingStatesToCancel(changedBaseEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledAddOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(changedBaseEntitlement, cancelledAddOnEntitlement, null);
+ // ...and for the "write" path (which has been exercised in the change call above).
+ checkActualBlockingStatesToCancel(changedBaseEntitlement, cancelledAddOnEntitlement, changeDateTime);
+ // Verify also the blocking states DAO doesn't add too many events (all on disk)
+ checkBlockingStatesDAO(changedBaseEntitlement, cancelledAddOnEntitlement, changeDate, false);
+
+ clock.addDays(30);
+ // No new event
+ assertTrue(testListener.isCompleted(DELAY));
+
+ checkFutureBlockingStatesToCancel(changedBaseEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledAddOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(changedBaseEntitlement, cancelledAddOnEntitlement, null);
+ checkActualBlockingStatesToCancel(changedBaseEntitlement, cancelledAddOnEntitlement, changeDateTime);
+ checkBlockingStatesDAO(changedBaseEntitlement, cancelledAddOnEntitlement, changeDate, false);
+ }
+
+ // Test the "read" path
+ private void checkFutureBlockingStatesToCancel(final DefaultEntitlement baseEntitlement, @Nullable final DefaultEntitlement addOnEntitlement, @Nullable final DateTime effectiveCancellationDateTime) throws EntitlementApiException {
+ final Collection<BlockingState> blockingStatesForCancellation = computeFutureBlockingStatesForAssociatedAddons(baseEntitlement);
+ if (addOnEntitlement == null || effectiveCancellationDateTime == null) {
+ Assert.assertEquals(blockingStatesForCancellation.size(), 0);
+ } else {
+ Assert.assertEquals(blockingStatesForCancellation.size(), 1);
+ final BlockingState blockingState = blockingStatesForCancellation.iterator().next();
+ Assert.assertEquals(blockingState.getBlockedId(), addOnEntitlement.getId());
+ Assert.assertEquals(blockingState.getEffectiveDate(), effectiveCancellationDateTime);
+ Assert.assertEquals(blockingState.getType(), BlockingStateType.SUBSCRIPTION);
+ Assert.assertEquals(blockingState.getService(), EntitlementService.ENTITLEMENT_SERVICE_NAME);
+ Assert.assertEquals(blockingState.getStateName(), DefaultEntitlementApi.ENT_STATE_CANCELLED);
+ }
+ }
+
+ // Test the "write" path
+ private void checkActualBlockingStatesToCancel(final DefaultEntitlement baseEntitlement, final DefaultEntitlement addOnEntitlement, @Nullable final DateTime effectiveCancellationDateTime) throws EntitlementApiException {
+ final Collection<BlockingState> blockingStatesForCancellation = computeBlockingStatesForAssociatedAddons(baseEntitlement, effectiveCancellationDateTime);
+ if (effectiveCancellationDateTime == null) {
+ Assert.assertEquals(blockingStatesForCancellation.size(), 0);
+ } else {
+ Assert.assertEquals(blockingStatesForCancellation.size(), 1);
+ final BlockingState blockingState = blockingStatesForCancellation.iterator().next();
+ Assert.assertEquals(blockingState.getBlockedId(), addOnEntitlement.getId());
+ Assert.assertEquals(blockingState.getEffectiveDate(), effectiveCancellationDateTime);
+ Assert.assertEquals(blockingState.getType(), BlockingStateType.SUBSCRIPTION);
+ Assert.assertEquals(blockingState.getService(), EntitlementService.ENTITLEMENT_SERVICE_NAME);
+ Assert.assertEquals(blockingState.getStateName(), DefaultEntitlementApi.ENT_STATE_CANCELLED);
+ }
+ }
+
+ // Test the DAO
+ private void checkBlockingStatesDAO(final Entitlement baseEntitlement, final Entitlement addOnEntitlement, final LocalDate effectiveCancellationDate, final boolean isBaseCancelled) {
+ final List<BlockingState> blockingStatesForBaseEntitlement = blockingStateDao.getBlockingAll(baseEntitlement.getId(), internalCallContext);
+ Assert.assertEquals(blockingStatesForBaseEntitlement.size(), isBaseCancelled ? 1 : 0);
+ if (isBaseCancelled) {
+ Assert.assertEquals(blockingStatesForBaseEntitlement.get(0).getBlockedId(), baseEntitlement.getId());
+ Assert.assertEquals(blockingStatesForBaseEntitlement.get(0).getEffectiveDate().toLocalDate(), effectiveCancellationDate);
+ Assert.assertEquals(blockingStatesForBaseEntitlement.get(0).getType(), BlockingStateType.SUBSCRIPTION);
+ Assert.assertEquals(blockingStatesForBaseEntitlement.get(0).getService(), EntitlementService.ENTITLEMENT_SERVICE_NAME);
+ Assert.assertEquals(blockingStatesForBaseEntitlement.get(0).getStateName(), DefaultEntitlementApi.ENT_STATE_CANCELLED);
+ }
+
+ final List<BlockingState> blockingStatesForAddOn = blockingStateDao.getBlockingAll(addOnEntitlement.getId(), internalCallContext);
+ Assert.assertEquals(blockingStatesForAddOn.size(), 1);
+ Assert.assertEquals(blockingStatesForAddOn.get(0).getBlockedId(), addOnEntitlement.getId());
+ Assert.assertEquals(blockingStatesForAddOn.get(0).getEffectiveDate().toLocalDate(), effectiveCancellationDate);
+ Assert.assertEquals(blockingStatesForAddOn.get(0).getType(), BlockingStateType.SUBSCRIPTION);
+ Assert.assertEquals(blockingStatesForAddOn.get(0).getService(), EntitlementService.ENTITLEMENT_SERVICE_NAME);
+ Assert.assertEquals(blockingStatesForAddOn.get(0).getStateName(), DefaultEntitlementApi.ENT_STATE_CANCELLED);
+ }
+
+ private Collection<BlockingState> computeFutureBlockingStatesForAssociatedAddons(final DefaultEntitlement baseEntitlement) throws EntitlementApiException {
+ return entitlementUtils.computeFutureBlockingStatesForAssociatedAddons(sqlDao.getBlockingHistoryForService(baseEntitlement.getId(), EntitlementService.ENTITLEMENT_SERVICE_NAME, internalCallContext),
+ baseEntitlement.getSubscriptionBase(),
+ clock.getUTCNow(),
+ internalCallContext);
+ }
+
+ private Collection<BlockingState> computeBlockingStatesForAssociatedAddons(final DefaultEntitlement baseEntitlement, final DateTime effectiveDate) throws EntitlementApiException {
+ return entitlementUtils.computeBlockingStatesForAssociatedAddons(baseEntitlement.getSubscriptionBase(), effectiveDate, internalCallContext);
+ }
+}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/EntitlementTestSuiteWithEmbeddedDB.java b/entitlement/src/test/java/com/ning/billing/entitlement/EntitlementTestSuiteWithEmbeddedDB.java
index 44cedca..b8b5768 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/EntitlementTestSuiteWithEmbeddedDB.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/EntitlementTestSuiteWithEmbeddedDB.java
@@ -30,6 +30,7 @@ import org.testng.annotations.BeforeMethod;
import com.ning.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
import com.ning.billing.account.api.AccountData;
+import com.ning.billing.account.api.AccountInternalApi;
import com.ning.billing.account.api.AccountUserApi;
import com.ning.billing.api.TestApiListener;
import com.ning.billing.api.TestListenerStatus;
@@ -42,13 +43,13 @@ import com.ning.billing.clock.ClockMock;
import com.ning.billing.entitlement.api.EntitlementApi;
import com.ning.billing.entitlement.api.SubscriptionApi;
import com.ning.billing.entitlement.dao.BlockingStateDao;
+import com.ning.billing.entitlement.engine.core.EntitlementUtils;
import com.ning.billing.entitlement.glue.TestEntitlementModuleWithEmbeddedDB;
+import com.ning.billing.junction.BlockingInternalApi;
import com.ning.billing.mock.MockAccountBuilder;
+import com.ning.billing.subscription.api.SubscriptionBaseInternalApi;
import com.ning.billing.subscription.api.SubscriptionBaseService;
import com.ning.billing.subscription.engine.core.DefaultSubscriptionBaseService;
-import com.ning.billing.account.api.AccountInternalApi;
-import com.ning.billing.junction.BlockingInternalApi;
-import com.ning.billing.subscription.api.SubscriptionBaseInternalApi;
import com.ning.billing.tag.TagInternalApi;
import com.ning.billing.util.svcsapi.bus.BusService;
import com.ning.billing.util.tag.dao.TagDao;
@@ -64,6 +65,9 @@ public class EntitlementTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWi
protected static final Logger log = LoggerFactory.getLogger(EntitlementTestSuiteWithEmbeddedDB.class);
+ // Be generous...
+ protected static final Long DELAY = 20000L;
+
@Inject
protected AccountUserApi accountApi;
@Inject
@@ -86,14 +90,18 @@ public class EntitlementTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWi
protected TagDao tagDao;
@Inject
protected TagInternalApi tagInternalApi;
- @javax.inject.Inject
+ @Inject
protected TestApiListener testListener;
- @javax.inject.Inject
+ @Inject
protected TestListenerStatus testListenerStatus;
- @javax.inject.Inject
+ @Inject
protected BusService busService;
- @javax.inject.Inject
+ @Inject
protected SubscriptionBaseService subscriptionBaseService;
+ @Inject
+ protected EntitlementService entitlementService;
+ @Inject
+ protected EntitlementUtils entitlementUtils;
protected Catalog catalog;
@@ -114,13 +122,13 @@ public class EntitlementTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWi
@BeforeMethod(groups = "slow")
public void beforeMethod() throws Exception {
super.beforeMethod();
- startTestFamework(testListener, testListenerStatus, clock, busService, subscriptionBaseService);
+ startTestFamework(testListener, testListenerStatus, clock, busService, subscriptionBaseService, entitlementService);
this.catalog = initCatalog(catalogService);
}
@AfterMethod(groups = "slow")
public void afterMethod() throws Exception {
- stopTestFramework(testListener, busService, subscriptionBaseService);
+ stopTestFramework(testListener, busService, subscriptionBaseService, entitlementService);
}
private Catalog initCatalog(final CatalogService catalogService) throws Exception {
@@ -133,10 +141,11 @@ public class EntitlementTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWi
private void startTestFamework(final TestApiListener testListener,
- final TestListenerStatus testListenerStatus,
- final ClockMock clock,
- final BusService busService,
- final SubscriptionBaseService subscriptionBaseService) throws Exception {
+ final TestListenerStatus testListenerStatus,
+ final ClockMock clock,
+ final BusService busService,
+ final SubscriptionBaseService subscriptionBaseService,
+ final EntitlementService entitlementService) throws Exception {
log.warn("STARTING TEST FRAMEWORK");
resetTestListener(testListener, testListenerStatus);
@@ -146,17 +155,20 @@ public class EntitlementTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWi
startBusAndRegisterListener(busService, testListener);
restartSubscriptionService(subscriptionBaseService);
+ restartEntitlementService(entitlementService);
log.warn("STARTED TEST FRAMEWORK");
}
private void stopTestFramework(final TestApiListener testListener,
- final BusService busService,
- final SubscriptionBaseService subscriptionBaseService) throws Exception {
+ final BusService busService,
+ final SubscriptionBaseService subscriptionBaseService,
+ final EntitlementService entitlementService) throws Exception {
log.warn("STOPPING TEST FRAMEWORK");
stopBusAndUnregisterListener(busService, testListener);
stopSubscriptionService(subscriptionBaseService);
+ stopEntitlementService(entitlementService);
log.warn("STOPPED TEST FRAMEWORK");
}
@@ -187,6 +199,12 @@ public class EntitlementTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWi
((DefaultSubscriptionBaseService) subscriptionBaseService).start();
}
+ private void restartEntitlementService(final EntitlementService entitlementService) {
+ // START NOTIFICATION QUEUE FOR ENTITLEMENT
+ ((DefaultEntitlementService) entitlementService).initialize();
+ ((DefaultEntitlementService) entitlementService).start();
+ }
+
private void stopBusAndUnregisterListener(final BusService busService, final TestApiListener testListener) throws Exception {
busService.getBus().unregister(testListener);
busService.getBus().stop();
@@ -196,6 +214,9 @@ public class EntitlementTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWi
((DefaultSubscriptionBaseService) subscriptionBaseService).stop();
}
+ private void stopEntitlementService(final EntitlementService entitlementService) throws Exception {
+ ((DefaultEntitlementService) entitlementService).stop();
+ }
protected AccountData getAccountData(final int billingDay) {
return new MockAccountBuilder().name(UUID.randomUUID().toString().substring(1, 8))
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/glue/TestEntitlementModuleNoDB.java b/entitlement/src/test/java/com/ning/billing/entitlement/glue/TestEntitlementModuleNoDB.java
index 83147c6..1b52b21 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/glue/TestEntitlementModuleNoDB.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/glue/TestEntitlementModuleNoDB.java
@@ -16,6 +16,9 @@
package com.ning.billing.entitlement.glue;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+
import com.ning.billing.GuicyKillbillTestNoDBModule;
import com.ning.billing.catalog.MockCatalogModule;
import com.ning.billing.entitlement.dao.BlockingStateDao;
@@ -24,8 +27,12 @@ import com.ning.billing.mock.glue.MockAccountModule;
import com.ning.billing.mock.glue.MockNonEntityDaoModule;
import com.ning.billing.mock.glue.MockSubscriptionModule;
import com.ning.billing.mock.glue.MockTagModule;
+import com.ning.billing.notificationq.MockNotificationQueueService;
+import com.ning.billing.notificationq.api.NotificationQueueConfig;
+import com.ning.billing.notificationq.api.NotificationQueueService;
import com.ning.billing.util.bus.InMemoryBusModule;
-import org.skife.config.ConfigSource;
+
+import com.google.common.collect.ImmutableMap;
public class TestEntitlementModuleNoDB extends TestEntitlementModule {
@@ -43,6 +50,7 @@ public class TestEntitlementModuleNoDB extends TestEntitlementModule {
install(new MockSubscriptionModule());
install(new MockCatalogModule());
install(new MockAccountModule());
+ installNotificationQueue();
}
@Override
@@ -50,4 +58,14 @@ public class TestEntitlementModuleNoDB extends TestEntitlementModule {
bind(BlockingStateDao.class).to(MockBlockingStateDao.class).asEagerSingleton();
}
+ private void installNotificationQueue() {
+ bind(NotificationQueueService.class).to(MockNotificationQueueService.class).asEagerSingleton();
+ configureNotificationQueueConfig();
+ }
+
+ protected void configureNotificationQueueConfig() {
+ final NotificationQueueConfig config = new ConfigurationObjectFactory(configSource).buildWithReplacements(NotificationQueueConfig.class,
+ ImmutableMap.<String, String>of("instanceName", "main"));
+ bind(NotificationQueueConfig.class).toInstance(config);
+ }
}
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index da8134d..e7417af 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>com.ning.billing</groupId>
- <version>0.5.4</version>
+ <version>0.5.6-SNAPSHOT</version>
</parent>
<artifactId>killbill</artifactId>
<version>0.8.1-SNAPSHOT</version>
diff --git a/subscription/src/main/java/com/ning/billing/subscription/api/SubscriptionBaseApiService.java b/subscription/src/main/java/com/ning/billing/subscription/api/SubscriptionBaseApiService.java
index 418110a..b6f5a7b 100644
--- a/subscription/src/main/java/com/ning/billing/subscription/api/SubscriptionBaseApiService.java
+++ b/subscription/src/main/java/com/ning/billing/subscription/api/SubscriptionBaseApiService.java
@@ -52,16 +52,19 @@ public interface SubscriptionBaseApiService {
public boolean uncancel(DefaultSubscriptionBase subscription, CallContext context)
throws SubscriptionBaseApiException;
- public boolean changePlan(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
- String priceList, CallContext context)
+ // Return the effective date of the change, null for immediate
+ public DateTime changePlan(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
+ String priceList, CallContext context)
throws SubscriptionBaseApiException;
- public boolean changePlanWithRequestedDate(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
- String priceList, DateTime requestedDate, CallContext context)
+ // Return the effective date of the change, null for immediate
+ public DateTime changePlanWithRequestedDate(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
+ String priceList, DateTime requestedDate, CallContext context)
throws SubscriptionBaseApiException;
- public boolean changePlanWithPolicy(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
- String priceList, BillingActionPolicy policy, CallContext context)
+ // Return the effective date of the change, null for immediate
+ public DateTime changePlanWithPolicy(DefaultSubscriptionBase subscription, String productName, BillingPeriod term,
+ String priceList, BillingActionPolicy policy, CallContext context)
throws SubscriptionBaseApiException;
public int cancelAddOnsIfRequired(final DefaultSubscriptionBase baseSubscription, final DateTime effectiveDate, final InternalCallContext context);
diff --git a/subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBase.java b/subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBase.java
index 34b5f26..4faf2f2 100644
--- a/subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBase.java
+++ b/subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBase.java
@@ -235,20 +235,20 @@ public class DefaultSubscriptionBase extends EntityBase implements SubscriptionB
}
@Override
- public boolean changePlan(final String productName, final BillingPeriod term, final String priceList,
- final CallContext context) throws SubscriptionBaseApiException {
+ public DateTime changePlan(final String productName, final BillingPeriod term, final String priceList,
+ final CallContext context) throws SubscriptionBaseApiException {
return apiService.changePlan(this, productName, term, priceList, context);
}
@Override
- public boolean changePlanWithDate(final String productName, final BillingPeriod term, final String priceList,
- final DateTime requestedDate, final CallContext context) throws SubscriptionBaseApiException {
+ public DateTime changePlanWithDate(final String productName, final BillingPeriod term, final String priceList,
+ final DateTime requestedDate, final CallContext context) throws SubscriptionBaseApiException {
return apiService.changePlanWithRequestedDate(this, productName, term, priceList, requestedDate, context);
}
@Override
- public boolean changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
- final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+ public DateTime changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+ final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
return apiService.changePlanWithPolicy(this, productName, term, priceList, policy, context);
}
diff --git a/subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
index 9123240..87fb575 100644
--- a/subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
+++ b/subscription/src/main/java/com/ning/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -277,8 +277,8 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
@Override
- public boolean changePlan(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
- final String priceList, final CallContext context)
+ public DateTime changePlan(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
+ final String priceList, final CallContext context)
throws SubscriptionBaseApiException {
final DateTime now = clock.getUTCNow();
final DateTime requestedDate = now;
@@ -297,8 +297,8 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
@Override
- public boolean changePlanWithRequestedDate(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
- final String priceList, final DateTime requestedDateWithMs, final CallContext context)
+ public DateTime changePlanWithRequestedDate(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
+ final String priceList, final DateTime requestedDateWithMs, final CallContext context)
throws SubscriptionBaseApiException {
final DateTime now = clock.getUTCNow();
final DateTime requestedDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
@@ -314,8 +314,8 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
}
@Override
- public boolean changePlanWithPolicy(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
- final String priceList, final BillingActionPolicy policy, final CallContext context)
+ public DateTime changePlanWithPolicy(final DefaultSubscriptionBase subscription, final String productName, final BillingPeriod term,
+ final String priceList, final BillingActionPolicy policy, final CallContext context)
throws SubscriptionBaseApiException {
final DateTime now = clock.getUTCNow();
final DateTime requestedDate = now;
@@ -355,14 +355,14 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
return planChangeResult;
}
- private boolean doChangePlan(final DefaultSubscriptionBase subscription,
- final String newProductName,
- final BillingPeriod newBillingPeriod,
- final String newPriceList,
- final DateTime now,
- final DateTime requestedDate,
- final DateTime effectiveDate,
- final CallContext context) throws SubscriptionBaseApiException, CatalogApiException {
+ private DateTime doChangePlan(final DefaultSubscriptionBase subscription,
+ final String newProductName,
+ final BillingPeriod newBillingPeriod,
+ final String newPriceList,
+ final DateTime now,
+ final DateTime requestedDate,
+ final DateTime effectiveDate,
+ final CallContext context) throws SubscriptionBaseApiException, CatalogApiException {
final Plan newPlan = catalogService.getFullCatalog().findPlan(newProductName, newBillingPeriod, newPriceList, effectiveDate, subscription.getStartDate());
@@ -399,7 +399,7 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
final boolean isChangeImmediate = subscription.getCurrentPlan().getProduct().getName().equals(newProductName) &&
subscription.getCurrentPlan().getBillingPeriod() == newBillingPeriod;
- return isChangeImmediate;
+ return isChangeImmediate ? null : effectiveDate;
}
diff --git a/subscription/src/test/java/com/ning/billing/subscription/api/user/TestUserApiError.java b/subscription/src/test/java/com/ning/billing/subscription/api/user/TestUserApiError.java
index 3774427..5761a86 100644
--- a/subscription/src/test/java/com/ning/billing/subscription/api/user/TestUserApiError.java
+++ b/subscription/src/test/java/com/ning/billing/subscription/api/user/TestUserApiError.java
@@ -40,6 +40,7 @@ import com.ning.billing.util.callcontext.TenantContext;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
public class TestUserApiError extends SubscriptionTestSuiteNoDB {
@@ -156,7 +157,7 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
assertEquals(subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.ANNUAL);
}
- assertTrue(subscription.changePlanWithPolicy("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, BillingActionPolicy.IMMEDIATE, callContext));
+ assertNull(subscription.changePlanWithPolicy("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, BillingActionPolicy.IMMEDIATE, callContext));
assertEquals(subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext).getCurrentPlan().getBillingPeriod(), BillingPeriod.MONTHLY);
}
diff --git a/util/src/test/java/com/ning/billing/api/TestApiListener.java b/util/src/test/java/com/ning/billing/api/TestApiListener.java
index cfce358..f6abed4 100644
--- a/util/src/test/java/com/ning/billing/api/TestApiListener.java
+++ b/util/src/test/java/com/ning/billing/api/TestApiListener.java
@@ -23,6 +23,14 @@ import java.util.concurrent.Callable;
import javax.inject.Inject;
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.events.BlockingTransitionInternalEvent;
import com.ning.billing.events.CustomFieldEvent;
import com.ning.billing.events.EffectiveEntitlementInternalEvent;
import com.ning.billing.events.EffectiveSubscriptionInternalEvent;
@@ -34,12 +42,6 @@ import com.ning.billing.events.PaymentPluginErrorInternalEvent;
import com.ning.billing.events.RepairSubscriptionInternalEvent;
import com.ning.billing.events.TagDefinitionInternalEvent;
import com.ning.billing.events.TagInternalEvent;
-import org.joda.time.DateTime;
-import org.skife.jdbi.v2.Handle;
-import org.skife.jdbi.v2.IDBI;
-import org.skife.jdbi.v2.tweak.HandleCallback;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import com.google.common.base.Joiner;
import com.google.common.eventbus.Subscribe;
@@ -81,6 +83,7 @@ public class TestApiListener {
PAUSE,
RESUME,
PHASE,
+ BLOCK,
INVOICE,
INVOICE_ADJUSTMENT,
PAYMENT,
@@ -105,10 +108,8 @@ public class TestApiListener {
notifyIfStackEmpty();
}
-
@Subscribe
public void handleEntitlementEvents(final EffectiveEntitlementInternalEvent eventEffective) {
-
log.info(String.format("Got entitlement event %s", eventEffective.toString()));
switch (eventEffective.getTransitionType()) {
case BLOCK_BUNDLE:
@@ -120,6 +121,12 @@ public class TestApiListener {
}
}
+ @Subscribe
+ public void handleEntitlementEvents(final BlockingTransitionInternalEvent event) {
+ log.info(String.format("Got entitlement event %s", event.toString()));
+ assertEqualsNicely(NextEvent.BLOCK);
+ notifyIfStackEmpty();
+ }
@Subscribe
public void handleSubscriptionEvents(final EffectiveSubscriptionInternalEvent eventEffective) {
@@ -216,6 +223,7 @@ public class TestApiListener {
assertEqualsNicely(NextEvent.PAYMENT_ERROR);
notifyIfStackEmpty();
}
+
@Subscribe
public void handlePaymentPluginErrorEvents(final PaymentPluginErrorInternalEvent event) {
log.info(String.format("Got PaymentPluginError event %s", event.toString()));
diff --git a/util/src/test/java/com/ning/billing/mock/MockProduct.java b/util/src/test/java/com/ning/billing/mock/MockProduct.java
index a7d091f..d1415e5 100644
--- a/util/src/test/java/com/ning/billing/mock/MockProduct.java
+++ b/util/src/test/java/com/ning/billing/mock/MockProduct.java
@@ -21,20 +21,27 @@ import com.ning.billing.catalog.api.Product;
import com.ning.billing.catalog.api.ProductCategory;
public class MockProduct implements Product {
+
private final String name;
private final ProductCategory category;
private final String catalogName;
+ private final Product[] included;
+ private final Product[] available;
public MockProduct() {
- name = "TestProduct";
- category = ProductCategory.BASE;
- catalogName = "Vehicules";
+ this("TestProduct", ProductCategory.BASE, "Vehicules");
}
public MockProduct(final String name, final ProductCategory category, final String catalogName) {
+ this(name, category, catalogName, null, null);
+ }
+
+ public MockProduct(final String name, final ProductCategory category, final String catalogName, final Product[] included, final Product[] available) {
this.name = name;
this.category = category;
this.catalogName = catalogName;
+ this.included = included;
+ this.available = available;
}
@Override
@@ -59,12 +66,12 @@ public class MockProduct implements Product {
@Override
public Product[] getAvailable() {
- throw new UnsupportedOperationException();
+ return available;
}
@Override
public Product[] getIncluded() {
- throw new UnsupportedOperationException();
+ return included;
}
public static MockProduct createBicycle() {
diff --git a/util/src/test/java/com/ning/billing/mock/MockSubscription.java b/util/src/test/java/com/ning/billing/mock/MockSubscription.java
index 01b6c0f..a663791 100644
--- a/util/src/test/java/com/ning/billing/mock/MockSubscription.java
+++ b/util/src/test/java/com/ning/billing/mock/MockSubscription.java
@@ -94,19 +94,19 @@ public class MockSubscription implements SubscriptionBase {
}
@Override
- public boolean changePlan(final String productName, final BillingPeriod term, final String priceList, final CallContext context) throws SubscriptionBaseApiException {
+ public DateTime changePlan(final String productName, final BillingPeriod term, final String priceList, final CallContext context) throws SubscriptionBaseApiException {
return sub.changePlan(productName, term, priceList, context);
}
@Override
- public boolean changePlanWithDate(final String productName, final BillingPeriod term, final String priceList, final DateTime requestedDate,
- final CallContext context) throws SubscriptionBaseApiException {
+ public DateTime changePlanWithDate(final String productName, final BillingPeriod term, final String priceList, final DateTime requestedDate,
+ final CallContext context) throws SubscriptionBaseApiException {
return sub.changePlanWithDate(productName, term, priceList, requestedDate, context);
}
@Override
- public boolean changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
- final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
+ public DateTime changePlanWithPolicy(final String productName, final BillingPeriod term, final String priceList,
+ final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
return sub.changePlanWithPolicy(productName, term, priceList, policy, context);
}