killbill-memoizeit
Changes
entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java 406(+406 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java 720(+38 -682)
entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementOrderingBase.java 53(+53 -0)
entitlement/src/main/java/org/killbill/billing/entitlement/api/SubscriptionEventOrdering.java 324(+324 -0)
Details
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java
new file mode 100644
index 0000000..18f9399
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.entitlement.api;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.entitlement.DefaultEntitlementService;
+import org.killbill.billing.entitlement.block.BlockingChecker.BlockingAggregator;
+import org.killbill.billing.entitlement.block.DefaultBlockingChecker.DefaultBlockingAggregator;
+import org.killbill.billing.junction.DefaultBlockingState;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+// Given an event stream (across one or multiple entitlements), insert the blocking events at the right place
+public class BlockingStateOrdering extends EntitlementOrderingBase {
+
+ private static final BlockingStateOrdering INSTANCE = new BlockingStateOrdering();
+
+ private BlockingStateOrdering() {}
+
+ public static void insertSorted(final Iterable<Entitlement> entitlements, final DateTimeZone accountTimeZone, final LinkedList<SubscriptionEvent> result) {
+ INSTANCE.computeEvents(entitlements, accountTimeZone, result);
+ }
+
+ private void computeEvents(final Iterable<Entitlement> entitlements, final DateTimeZone accountTimeZone, final LinkedList<SubscriptionEvent> result) {
+ final Collection<UUID> allEntitlementUUIDs = new HashSet<UUID>();
+ final Collection<BlockingState> blockingStates = new LinkedList<BlockingState>();
+ for (final Entitlement entitlement : entitlements) {
+ allEntitlementUUIDs.add(entitlement.getId());
+ Preconditions.checkState(entitlement instanceof DefaultEntitlement, "Entitlement %s is not a DefaultEntitlement", entitlement);
+ blockingStates.addAll(((DefaultEntitlement) entitlement).getEventsStream().getBlockingStates());
+ }
+
+ // Trust the incoming ordering here: blocking states were sorted using ProxyBlockingStateDao#sortedCopy
+ for (final BlockingState bs : blockingStates) {
+ final List<SubscriptionEvent> newEvents = new ArrayList<SubscriptionEvent>();
+ final int index = insertFromBlockingEvent(accountTimeZone, allEntitlementUUIDs, result, bs, bs.getEffectiveDate(), newEvents);
+ insertAfterIndex(result, newEvents, index);
+ }
+ }
+
+ // Returns the index and the newEvents generated from the incoming blocking state event. Those new events will all be created for the same effectiveDate and should be ordered.
+ private int insertFromBlockingEvent(final DateTimeZone accountTimeZone, final Collection<UUID> allEntitlementUUIDs, final List<SubscriptionEvent> result, final BlockingState bs, final DateTime bsEffectiveDate, final List<SubscriptionEvent> newEvents) {
+ // Keep the current state per entitlement
+ final Map<UUID, TargetState> targetStates = new HashMap<UUID, TargetState>();
+ for (final UUID cur : allEntitlementUUIDs) {
+ targetStates.put(cur, new TargetState());
+ }
+
+ //
+ // Find out where to insert next event, and calculate current state for each entitlement at the position where we stop.
+ //
+ int index = -1;
+ final Iterator<SubscriptionEvent> it = result.iterator();
+ // Where we need to insert in that stream
+ DefaultSubscriptionEvent curInsertion = null;
+ while (it.hasNext()) {
+ final DefaultSubscriptionEvent cur = (DefaultSubscriptionEvent) it.next();
+ final int compEffectiveDate = bsEffectiveDate.compareTo(cur.getEffectiveDateTime());
+ final boolean shouldContinue = (compEffectiveDate >= 0);
+ if (!shouldContinue) {
+ break;
+ }
+ index++;
+
+ final TargetState curTargetState = targetStates.get(cur.getEntitlementId());
+ switch (cur.getSubscriptionEventType()) {
+ case START_ENTITLEMENT:
+ curTargetState.setEntitlementStarted();
+ break;
+ case STOP_ENTITLEMENT:
+ curTargetState.setEntitlementStopped();
+ break;
+ case START_BILLING:
+ curTargetState.setBillingStarted();
+ break;
+ case PAUSE_BILLING:
+ case PAUSE_ENTITLEMENT:
+ case RESUME_ENTITLEMENT:
+ case RESUME_BILLING:
+ case SERVICE_STATE_CHANGE:
+ curTargetState.addEntitlementEvent(cur);
+ break;
+ case STOP_BILLING:
+ curTargetState.setBillingStopped();
+ break;
+ }
+ curInsertion = cur;
+ }
+
+ // Extract the list of targets based on the type of blocking state
+ final List<UUID> targetEntitlementIds = bs.getType() == BlockingStateType.SUBSCRIPTION ? ImmutableList.<UUID>of(bs.getBlockedId()) :
+ ImmutableList.<UUID>copyOf(allEntitlementUUIDs);
+
+ // For each target compute the new events that should be inserted in the stream
+ for (final UUID targetEntitlementId : targetEntitlementIds) {
+ final SubscriptionEvent[] prevNext = findPrevNext(result, targetEntitlementId, curInsertion);
+ final TargetState curTargetState = targetStates.get(targetEntitlementId);
+
+ final List<SubscriptionEventType> eventTypes = curTargetState.addStateAndReturnEventTypes(bs);
+ for (final SubscriptionEventType t : eventTypes) {
+ newEvents.add(toSubscriptionEvent(prevNext[0], prevNext[1], targetEntitlementId, bs, t, accountTimeZone));
+ }
+ }
+ return index;
+ }
+
+ // Extract prev and next events in the stream events for that particular target subscription from the insertionEvent
+ private SubscriptionEvent[] findPrevNext(final List<SubscriptionEvent> events, final UUID targetEntitlementId, final SubscriptionEvent insertionEvent) {
+ // Find prev/next event for the same entitlement
+ final SubscriptionEvent[] result = new DefaultSubscriptionEvent[2];
+ if (insertionEvent == null) {
+ result[0] = null;
+ result[1] = !events.isEmpty() ? events.get(0) : null;
+ return result;
+ }
+
+ final Iterator<SubscriptionEvent> it = events.iterator();
+ DefaultSubscriptionEvent prev = null;
+ DefaultSubscriptionEvent next = null;
+ boolean foundCur = false;
+ while (it.hasNext()) {
+ final DefaultSubscriptionEvent tmp = (DefaultSubscriptionEvent) it.next();
+ if (tmp.getEntitlementId().equals(targetEntitlementId)) {
+ if (!foundCur) {
+ prev = tmp;
+ } else {
+ next = tmp;
+ break;
+ }
+ }
+ // Check both the id and the event type because of multiplexing
+ if (tmp.getId().equals(insertionEvent.getId()) &&
+ tmp.getSubscriptionEventType().equals(insertionEvent.getSubscriptionEventType())) {
+ foundCur = true;
+ }
+ }
+ result[0] = prev;
+ result[1] = next;
+ return result;
+ }
+
+ private SubscriptionEvent toSubscriptionEvent(@Nullable final SubscriptionEvent prev, @Nullable final SubscriptionEvent next,
+ final UUID entitlementId, final BlockingState in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
+ final Product prevProduct;
+ final Plan prevPlan;
+ final PlanPhase prevPlanPhase;
+ final PriceList prevPriceList;
+ final BillingPeriod prevBillingPeriod;
+ // Enforce prev = null for start events
+ if (prev == null || SubscriptionEventType.START_ENTITLEMENT.equals(eventType) || SubscriptionEventType.START_BILLING.equals(eventType)) {
+ prevProduct = null;
+ prevPlan = null;
+ prevPlanPhase = null;
+ prevPriceList = null;
+ prevBillingPeriod = null;
+ } else {
+ // We look for the next for the 'prev' meaning we we are headed to, but if this is null -- for example on cancellation we get the prev which gives the correct state.
+ prevProduct = (prev.getNextProduct() != null ? prev.getNextProduct() : prev.getPrevProduct());
+ prevPlan = (prev.getNextPlan() != null ? prev.getNextPlan() : prev.getPrevPlan());
+ prevPlanPhase = (prev.getNextPhase() != null ? prev.getNextPhase() : prev.getPrevPhase());
+ prevPriceList = (prev.getNextPriceList() != null ? prev.getNextPriceList() : prev.getPrevPriceList());
+ prevBillingPeriod = (prev.getNextBillingPeriod() != null ? prev.getNextBillingPeriod() : prev.getPrevBillingPeriod());
+ }
+
+ final Product nextProduct;
+ final Plan nextPlan;
+ final PlanPhase nextPlanPhase;
+ final PriceList nextPriceList;
+ final BillingPeriod nextBillingPeriod;
+ if (SubscriptionEventType.PAUSE_ENTITLEMENT.equals(eventType) || SubscriptionEventType.PAUSE_BILLING.equals(eventType) ||
+ SubscriptionEventType.RESUME_ENTITLEMENT.equals(eventType) || SubscriptionEventType.RESUME_BILLING.equals(eventType) ||
+ (SubscriptionEventType.SERVICE_STATE_CHANGE.equals(eventType) && (prev == null || (!SubscriptionEventType.STOP_ENTITLEMENT.equals(prev.getSubscriptionEventType()) && !SubscriptionEventType.STOP_BILLING.equals(prev.getSubscriptionEventType()))))) {
+ // Enforce next = prev for pause/resume events as well as service changes
+ nextProduct = prevProduct;
+ nextPlan = prevPlan;
+ nextPlanPhase = prevPlanPhase;
+ nextPriceList = prevPriceList;
+ nextBillingPeriod = prevBillingPeriod;
+ } else if (next == null) {
+ // Enforce next = null for stop events
+ if (prev == null || SubscriptionEventType.STOP_ENTITLEMENT.equals(eventType) || SubscriptionEventType.STOP_BILLING.equals(eventType)) {
+ nextProduct = null;
+ nextPlan = null;
+ nextPlanPhase = null;
+ nextPriceList = null;
+ nextBillingPeriod = null;
+ } else {
+ nextProduct = prev.getNextProduct();
+ nextPlan = prev.getNextPlan();
+ nextPlanPhase = prev.getNextPhase();
+ nextPriceList = prev.getNextPriceList();
+ nextBillingPeriod = prev.getNextBillingPeriod();
+ }
+ } else {
+ nextProduct = next.getNextProduct();
+ nextPlan = next.getNextPlan();
+ nextPlanPhase = next.getNextPhase();
+ nextPriceList = next.getNextPriceList();
+ nextBillingPeriod = next.getNextBillingPeriod();
+ }
+
+ // See https://github.com/killbill/killbill/issues/135
+ final String serviceName = getRealServiceNameForEntitlementOrExternalServiceName(in.getService(), eventType);
+
+ return new DefaultSubscriptionEvent(in.getId(),
+ entitlementId,
+ in.getEffectiveDate(),
+ in.getCreatedDate(),
+ eventType,
+ in.isBlockEntitlement(),
+ in.isBlockBilling(),
+ serviceName,
+ in.getStateName(),
+ prevProduct,
+ prevPlan,
+ prevPlanPhase,
+ prevPriceList,
+ prevBillingPeriod,
+ nextProduct,
+ nextPlan,
+ nextPlanPhase,
+ nextPriceList,
+ nextBillingPeriod,
+ in.getCreatedDate(),
+ accountTimeZone);
+ }
+
+ private void insertAfterIndex(final LinkedList<SubscriptionEvent> original, final Collection<SubscriptionEvent> newEvents, final int index) {
+ final boolean firstPosition = (index == -1);
+ final boolean lastPosition = (index == original.size() - 1);
+ if (lastPosition || firstPosition) {
+ for (final SubscriptionEvent cur : newEvents) {
+ if (lastPosition) {
+ original.addLast(cur);
+ } else {
+ original.addFirst(cur);
+ }
+ }
+ } else {
+ original.addAll(index + 1, newEvents);
+ }
+ }
+
+ //
+ // Internal class to keep the state associated with each subscription
+ //
+ private static final class TargetState {
+
+ private final Map<String, BlockingState> perServiceBlockingState;
+
+ private boolean isEntitlementStarted;
+ private boolean isEntitlementStopped;
+ private boolean isBillingStarted;
+ private boolean isBillingStopped;
+
+ public TargetState() {
+ this.isEntitlementStarted = false;
+ this.isEntitlementStopped = false;
+ this.isBillingStarted = false;
+ this.isBillingStopped = false;
+ this.perServiceBlockingState = new HashMap<String, BlockingState>();
+ }
+
+ public void setEntitlementStarted() {
+ isEntitlementStarted = true;
+ }
+
+ public void setEntitlementStopped() {
+ isEntitlementStopped = true;
+ }
+
+ public void setBillingStarted() {
+ isBillingStarted = true;
+ }
+
+ public void setBillingStopped() {
+ isBillingStopped = true;
+ }
+
+ public void addEntitlementEvent(final SubscriptionEvent e) {
+ final String serviceName = getRealServiceNameForEntitlementOrExternalServiceName(e.getServiceName(), e.getSubscriptionEventType());
+ final BlockingState lastBlockingStateForService = perServiceBlockingState.get(serviceName);
+
+ // Assume the event has no impact on changes - TODO this is wrong for SERVICE_STATE_CHANGE
+ final boolean blockChange = lastBlockingStateForService != null && lastBlockingStateForService.isBlockChange();
+ // For block entitlement or billing, override the previous state
+ final boolean blockedEntitlement = e.isBlockedEntitlement();
+ final boolean blockedBilling = e.isBlockedBilling();
+
+ final BlockingState converted = new DefaultBlockingState(e.getEntitlementId(),
+ BlockingStateType.SUBSCRIPTION,
+ e.getServiceStateName(),
+ serviceName,
+ blockChange,
+ blockedEntitlement,
+ blockedBilling,
+ ((DefaultSubscriptionEvent) e).getEffectiveDateTime());
+ perServiceBlockingState.put(converted.getService(), converted);
+ }
+
+ //
+ // From the current state of that subscription, compute the effect of the new state based on the incoming blockingState event
+ //
+ private List<SubscriptionEventType> addStateAndReturnEventTypes(final BlockingState bs) {
+ // Turn off isBlockedEntitlement and isBlockedBilling if there was not start event
+ final BlockingState fixedBlockingState = new DefaultBlockingState(bs.getBlockedId(),
+ bs.getType(),
+ bs.getStateName(),
+ bs.getService(),
+ bs.isBlockChange(),
+ (bs.isBlockEntitlement() && isEntitlementStarted && !isEntitlementStopped),
+ (bs.isBlockBilling() && isBillingStarted && !isBillingStopped),
+ bs.getEffectiveDate());
+
+ final List<SubscriptionEventType> result = new ArrayList<SubscriptionEventType>(4);
+ if (fixedBlockingState.getStateName().equals(DefaultEntitlementApi.ENT_STATE_CANCELLED)) {
+ isEntitlementStopped = true;
+ result.add(SubscriptionEventType.STOP_ENTITLEMENT);
+ return result;
+ }
+
+ //
+ // We look at the effect of the incoming event for the specific service, and then recompute the state after so we can compare if anything has changed
+ // across all services
+ //
+ final BlockingAggregator stateBefore = getState();
+ if (DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(fixedBlockingState.getService())) {
+ // Some blocking states will be added as entitlement-service and billing-service via addEntitlementEvent
+ // (see above). Because of it, we need to multiplex entitlement events here.
+ // TODO - this is magic and fragile. We should revisit how we create this state machine.
+ perServiceBlockingState.put(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME, fixedBlockingState);
+ perServiceBlockingState.put(BILLING_SERVICE_NAME, fixedBlockingState);
+ } else {
+ perServiceBlockingState.put(fixedBlockingState.getService(), fixedBlockingState);
+ }
+ final BlockingAggregator stateAfter = getState();
+
+ final boolean shouldResumeEntitlement = isEntitlementStarted && !isEntitlementStopped && stateBefore.isBlockEntitlement() && !stateAfter.isBlockEntitlement();
+ if (shouldResumeEntitlement) {
+ result.add(SubscriptionEventType.RESUME_ENTITLEMENT);
+ }
+ final boolean shouldResumeBilling = isBillingStarted && !isBillingStopped && stateBefore.isBlockBilling() && !stateAfter.isBlockBilling();
+ if (shouldResumeBilling) {
+ result.add(SubscriptionEventType.RESUME_BILLING);
+ }
+
+ final boolean shouldBlockEntitlement = isEntitlementStarted && !isEntitlementStopped && !stateBefore.isBlockEntitlement() && stateAfter.isBlockEntitlement();
+ if (shouldBlockEntitlement) {
+ result.add(SubscriptionEventType.PAUSE_ENTITLEMENT);
+ }
+ final boolean shouldBlockBilling = isBillingStarted && !isBillingStopped && !stateBefore.isBlockBilling() && stateAfter.isBlockBilling();
+ if (shouldBlockBilling) {
+ result.add(SubscriptionEventType.PAUSE_BILLING);
+ }
+
+ if (!shouldResumeEntitlement && !shouldResumeBilling && !shouldBlockEntitlement && !shouldBlockBilling && !fixedBlockingState.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
+ result.add(SubscriptionEventType.SERVICE_STATE_CHANGE);
+ }
+ return result;
+ }
+
+ private BlockingAggregator getState() {
+ final DefaultBlockingAggregator aggrBefore = new DefaultBlockingAggregator();
+ for (final BlockingState cur : perServiceBlockingState.values()) {
+ aggrBefore.or(cur);
+ }
+ return aggrBefore;
+ }
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java
index e422e72..57fc554 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
*
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
*
@@ -17,6 +19,7 @@
package org.killbill.billing.entitlement.api;
import java.util.Collection;
+import java.util.List;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
@@ -78,4 +81,9 @@ public class DefaultSubscription extends DefaultEntitlement implements Subscript
return blockingState == null ? null : blockingState.getService();
}
}
+
+ @Override
+ public List<SubscriptionEvent> getSubscriptionEvents() {
+ return SubscriptionEventOrdering.sortedCopy(this, getAccountTimeZone());
+ }
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
index ecad08e..91e5758 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
@@ -18,588 +18,23 @@
package org.killbill.billing.entitlement.api;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
import java.util.UUID;
-import javax.annotation.Nullable;
-
-import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
-import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.catalog.api.Plan;
-import org.killbill.billing.catalog.api.PlanPhase;
-import org.killbill.billing.catalog.api.PriceList;
-import org.killbill.billing.catalog.api.Product;
-import org.killbill.billing.entitlement.DefaultEntitlementService;
-import org.killbill.billing.entitlement.block.BlockingChecker.BlockingAggregator;
-import org.killbill.billing.entitlement.block.DefaultBlockingChecker.DefaultBlockingAggregator;
-import org.killbill.billing.junction.DefaultBlockingState;
-import org.killbill.billing.subscription.api.SubscriptionBase;
-import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
-import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTimeline {
- private final Logger logger = LoggerFactory.getLogger(DefaultSubscriptionBundleTimeline.class);
-
- public static final String BILLING_SERVICE_NAME = "billing-service";
- public static final String ENT_BILLING_SERVICE_NAME = "entitlement+billing-service";
-
- private final List<SubscriptionEvent> events;
private final UUID accountId;
private final UUID bundleId;
private final String externalKey;
+ private final List<SubscriptionEvent> events;
- public DefaultSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final Collection<Entitlement> entitlements) {
- // Trust the incoming ordering here: blocking states were sorted using ProxyBlockingStateDao#sortedCopy
- final List<BlockingState> blockingStates = new LinkedList<BlockingState>();
- for (final Entitlement entitlement : entitlements) {
- blockingStates.addAll(((DefaultEntitlement) entitlement).getEventsStream().getBlockingStates());
- }
- this.accountId = accountId;
- this.bundleId = bundleId;
- this.externalKey = externalKey;
- this.events = computeEvents(entitlements, blockingStates, accountTimeZone);
- }
-
- @VisibleForTesting
- DefaultSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final Collection<Entitlement> entitlements, final List<BlockingState> allBlockingStates) {
+ public DefaultSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final Iterable<Entitlement> entitlements) {
this.accountId = accountId;
this.bundleId = bundleId;
this.externalKey = externalKey;
- this.events = computeEvents(entitlements, allBlockingStates, accountTimeZone);
- }
-
- //
- // Compute all events based on blocking states events and base subscription events
- // Note that:
- // - base subscription events are already ordered for each Entitlement and so when we reorder at the bundle level we try not to break that initial ordering
- // - blocking state events occur at various level (account, bundle and subscription) so for higher level, we need to dispatch that on each subscription.
- //
- private List<SubscriptionEvent> computeEvents(final Collection<Entitlement> entitlements, final List<BlockingState> allBlockingStates, final DateTimeZone accountTimeZone) {
- // Extract ids for all entitlement in the list
- final Set<UUID> allEntitlementUUIDs = new TreeSet<UUID>(Collections2.transform(entitlements, new Function<Entitlement, UUID>() {
- @Override
- public UUID apply(final Entitlement input) {
- return input.getId();
- }
- }));
-
- // Compute base events across all entitlements
- final LinkedList<SubscriptionEvent> result = computeSubscriptionBaseEvents(entitlements, accountTimeZone);
-
- for (final BlockingState bs : allBlockingStates) {
- final List<SubscriptionEvent> newEvents = new ArrayList<SubscriptionEvent>();
- final int index = insertFromBlockingEvent(accountTimeZone, allEntitlementUUIDs, result, bs, bs.getEffectiveDate(), newEvents);
- insertAfterIndex(result, newEvents, index);
- }
-
- reOrderSubscriptionEventsOnSameDateByType(result);
-
- removeOverlappingSubscriptionEvents(result);
-
- return result;
- }
-
- // Make sure the argument supports the remove operation - hence expect a LinkedList, not a List
- private void removeOverlappingSubscriptionEvents(final LinkedList<SubscriptionEvent> events) {
- final Iterator<SubscriptionEvent> iterator = events.iterator();
- final Map<String, DefaultSubscriptionEvent> prevPerService = new HashMap<String, DefaultSubscriptionEvent>();
- while (iterator.hasNext()) {
- final DefaultSubscriptionEvent current = (DefaultSubscriptionEvent) iterator.next();
- final DefaultSubscriptionEvent prev = prevPerService.get(current.getServiceName());
- if (prev != null) {
- if (current.overlaps(prev)) {
- iterator.remove();
- } else {
- prevPerService.put(current.getServiceName(), current);
- }
- } else {
- prevPerService.put(current.getServiceName(), current);
- }
- }
- }
-
- //
- // All events have been inserted and should be at the right place, except that we want to ensure that events for a given subscription,
- // and for a given time are ordered by SubscriptionEventType.
- //
- // All this seems a little over complicated, and one wonders why we don't just shove all events and call Collections.sort on the list prior
- // to return:
- // - One explanation is that we don't know the events in advance and each time the new events to be inserted are computed from the current state
- // of the stream, which requires ordering all along
- // - A careful reader will notice that the algorithm is N^2, -- so that we care so much considering we have very events -- but in addition to that
- // the recursive path will be used very infrequently and when it is used, this will be probably just reorder with the prev event and that's it.
- //
- @VisibleForTesting
- protected void reOrderSubscriptionEventsOnSameDateByType(final List<SubscriptionEvent> events) {
- final int size = events.size();
- for (int i = 0; i < size; i++) {
- final SubscriptionEvent cur = events.get(i);
- final SubscriptionEvent next = (i < (size - 1)) ? events.get(i + 1) : null;
-
- final boolean shouldSwap = (next != null && shouldSwap(cur, next, true));
- final boolean shouldReverseSort = (next == null || shouldSwap);
-
- int currentIndex = i;
- if (shouldSwap) {
- Collections.swap(events, i, i + 1);
- }
- if (shouldReverseSort) {
- while (currentIndex >= 1) {
- final SubscriptionEvent revCur = events.get(currentIndex);
- final SubscriptionEvent other = events.get(currentIndex - 1);
- if (shouldSwap(revCur, other, false)) {
- Collections.swap(events, currentIndex, currentIndex - 1);
- }
- if (revCur.getEffectiveDate().compareTo(other.getEffectiveDate()) != 0) {
- break;
- }
- currentIndex--;
- }
- }
- }
- }
-
- private Integer compareSubscriptionEventsForSameEffectiveDateAndEntitlementId(final SubscriptionEvent first, final SubscriptionEvent second) {
- // For consistency, make sure entitlement-service and billing-service events always happen in a
- // deterministic order (e.g. after other services for STOP events and before for START events)
- if ((DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(first.getServiceName()) ||
- DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME.equals(first.getServiceName())) &&
- !(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(second.getServiceName()) ||
- DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME.equals(second.getServiceName()))) {
- // first is an entitlement-service or billing-service event, but not second
- if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT) ||
- first.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING) ||
- first.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_ENTITLEMENT) ||
- first.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_BILLING) ||
- first.getSubscriptionEventType().equals(SubscriptionEventType.PHASE) ||
- first.getSubscriptionEventType().equals(SubscriptionEventType.CHANGE)) {
- return -1;
- } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_ENTITLEMENT) ||
- first.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_BILLING) ||
- first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT) ||
- first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
- return 1;
- } else {
- // Default behavior
- return -1;
- }
- } else if ((DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(second.getServiceName()) ||
- DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME.equals(second.getServiceName())) &&
- !(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(first.getServiceName()) ||
- DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME.equals(first.getServiceName()))) {
- // second is an entitlement-service or billing-service event, but not first
- if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT) ||
- second.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING) ||
- second.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_ENTITLEMENT) ||
- second.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_BILLING) ||
- second.getSubscriptionEventType().equals(SubscriptionEventType.PHASE) ||
- second.getSubscriptionEventType().equals(SubscriptionEventType.CHANGE)) {
- return 1;
- } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_ENTITLEMENT) ||
- second.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_BILLING) ||
- second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT) ||
- second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
- return -1;
- } else {
- // Default behavior
- return 1;
- }
- } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT)) {
- // START_ENTITLEMENT is always first
- return -1;
- } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT)) {
- // START_ENTITLEMENT is always first
- return 1;
- } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
- // STOP_BILLING is always last
- return 1;
- } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
- // STOP_BILLING is always last
- return -1;
- } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING)) {
- // START_BILLING is first after START_ENTITLEMENT
- return -1;
- } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING)) {
- // START_BILLING is first after START_ENTITLEMENT
- return 1;
- } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT)) {
- // STOP_ENTITLEMENT is last after STOP_BILLING
- return 1;
- } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT)) {
- // STOP_ENTITLEMENT is last after STOP_BILLING
- return -1;
- } else {
- // Trust the current ordering
- return null;
- }
- }
-
- private boolean shouldSwap(final SubscriptionEvent cur, final SubscriptionEvent other, final boolean isAscending) {
- // For a given date, order by subscriptionId, and within subscription by event type
- final int idComp = cur.getEntitlementId().compareTo(other.getEntitlementId());
- final Integer comparison = compareSubscriptionEventsForSameEffectiveDateAndEntitlementId(cur, other);
- return (cur.getEffectiveDate().compareTo(other.getEffectiveDate()) == 0 &&
- ((isAscending &&
- ((idComp > 0) ||
- (idComp == 0 && comparison != null && comparison > 0))) ||
- (!isAscending &&
- ((idComp < 0) ||
- (idComp == 0 && comparison != null && comparison < 0)))));
- }
-
- private void insertAfterIndex(final LinkedList<SubscriptionEvent> original, final List<SubscriptionEvent> newEvents, final int index) {
- final boolean firstPosition = (index == -1);
- final boolean lastPosition = (index == original.size() - 1);
- if (lastPosition || firstPosition) {
- for (final SubscriptionEvent cur : newEvents) {
- if (lastPosition) {
- original.addLast(cur);
- } else {
- original.addFirst(cur);
- }
- }
- } else {
- original.addAll(index + 1, newEvents);
- }
- }
-
- //
- // Returns the index and the newEvents generated from the incoming blocking state event. Those new events will all created for the same effectiveDate and should be ordered but
- // reOrderSubscriptionEventsOnSameDateByType would reorder them anyway if this was not the case.
- //
- private int insertFromBlockingEvent(final DateTimeZone accountTimeZone, final Set<UUID> allEntitlementUUIDs, final List<SubscriptionEvent> result, final BlockingState bs, final DateTime bsEffectiveDate, final List<SubscriptionEvent> newEvents) {
- // Keep the current state per entitlement
- final Map<UUID, TargetState> targetStates = new HashMap<UUID, TargetState>();
- for (final UUID cur : allEntitlementUUIDs) {
- targetStates.put(cur, new TargetState());
- }
-
- //
- // Find out where to insert next event, and calculate current state for each entitlement at the position where we stop.
- //
- int index = -1;
- final Iterator<SubscriptionEvent> it = result.iterator();
- // Where we need to insert in that stream
- DefaultSubscriptionEvent curInsertion = null;
- while (it.hasNext()) {
- final DefaultSubscriptionEvent cur = (DefaultSubscriptionEvent) it.next();
- final int compEffectiveDate = bsEffectiveDate.compareTo(cur.getEffectiveDateTime());
- final boolean shouldContinue = (compEffectiveDate >= 0);
- if (!shouldContinue) {
- break;
- }
- index++;
-
- final TargetState curTargetState = targetStates.get(cur.getEntitlementId());
- switch (cur.getSubscriptionEventType()) {
- case START_ENTITLEMENT:
- curTargetState.setEntitlementStarted();
- break;
- case STOP_ENTITLEMENT:
- curTargetState.setEntitlementStopped();
- break;
- case START_BILLING:
- curTargetState.setBillingStarted();
- break;
- case PAUSE_BILLING:
- case PAUSE_ENTITLEMENT:
- case RESUME_ENTITLEMENT:
- case RESUME_BILLING:
- case SERVICE_STATE_CHANGE:
- curTargetState.addEntitlementEvent(cur);
- break;
- case STOP_BILLING:
- curTargetState.setBillingStopped();
- break;
- }
- curInsertion = cur;
- }
-
- // Extract the list of targets based on the type of blocking state
- final List<UUID> targetEntitlementIds = bs.getType() == BlockingStateType.SUBSCRIPTION ? ImmutableList.<UUID>of(bs.getBlockedId()) :
- ImmutableList.<UUID>copyOf(allEntitlementUUIDs);
-
- // For each target compute the new events that should be inserted in the stream
- for (final UUID targetEntitlementId : targetEntitlementIds) {
- final SubscriptionEvent[] prevNext = findPrevNext(result, targetEntitlementId, curInsertion);
- final TargetState curTargetState = targetStates.get(targetEntitlementId);
-
- final List<SubscriptionEventType> eventTypes = curTargetState.addStateAndReturnEventTypes(bs);
- for (final SubscriptionEventType t : eventTypes) {
- newEvents.add(toSubscriptionEvent(prevNext[0], prevNext[1], targetEntitlementId, bs, t, accountTimeZone));
- }
- }
- return index;
- }
-
- // Extract prev and next events in the stream events for that particular target subscription from the insertionEvent
- private SubscriptionEvent[] findPrevNext(final List<SubscriptionEvent> events, final UUID targetEntitlementId, final SubscriptionEvent insertionEvent) {
- // Find prev/next event for the same entitlement
- final SubscriptionEvent[] result = new DefaultSubscriptionEvent[2];
- if (insertionEvent == null) {
- result[0] = null;
- result[1] = !events.isEmpty() ? events.get(0) : null;
- return result;
- }
-
- final Iterator<SubscriptionEvent> it = events.iterator();
- DefaultSubscriptionEvent prev = null;
- DefaultSubscriptionEvent next = null;
- boolean foundCur = false;
- while (it.hasNext()) {
- final DefaultSubscriptionEvent tmp = (DefaultSubscriptionEvent) it.next();
- if (tmp.getEntitlementId().equals(targetEntitlementId)) {
- if (!foundCur) {
- prev = tmp;
- } else {
- next = tmp;
- break;
- }
- }
- // Check both the id and the event type because of multiplexing
- if (tmp.getId().equals(insertionEvent.getId()) &&
- tmp.getSubscriptionEventType().equals(insertionEvent.getSubscriptionEventType())) {
- foundCur = true;
- }
- }
- result[0] = prev;
- result[1] = next;
- return result;
- }
-
- // Compute the initial stream of events based on the subscription base events
- private LinkedList<SubscriptionEvent> computeSubscriptionBaseEvents(final Collection<Entitlement> entitlements, final DateTimeZone accountTimeZone) {
- final LinkedList<SubscriptionEvent> result = new LinkedList<SubscriptionEvent>();
- for (final Entitlement cur : entitlements) {
- final SubscriptionBase base = ((DefaultEntitlement) cur).getSubscriptionBase();
- final List<SubscriptionBaseTransition> baseTransitions = base.getAllTransitions();
- for (final SubscriptionBaseTransition tr : baseTransitions) {
- final List<SubscriptionEventType> eventTypes = toEventTypes(tr.getTransitionType());
- for (final SubscriptionEventType eventType : eventTypes) {
- final SubscriptionEvent event = toSubscriptionEvent(tr, eventType, accountTimeZone);
- insertSubscriptionEvent(event, result);
- }
- }
- }
-
- return result;
- }
-
- private void insertSubscriptionEvent(final SubscriptionEvent event, final List<SubscriptionEvent> result) {
- int index = 0;
- for (final SubscriptionEvent cur : result) {
- final int compEffectiveDate = event.getEffectiveDate().compareTo(cur.getEffectiveDate());
- if (compEffectiveDate < 0) {
- // EffectiveDate is less than cur -> insert here
- break;
- } else if (compEffectiveDate == 0) {
- final int compUUID = event.getEntitlementId().compareTo(cur.getEntitlementId());
- if (compUUID < 0) {
- // Same EffectiveDate but subscription are different, no need top sort further just return something deterministic
- break;
- } else if (compUUID == 0) {
- final int eventOrder = event.getSubscriptionEventType().ordinal() - cur.getSubscriptionEventType().ordinal();
- if (eventOrder < 0) {
- // Same EffectiveDate and same subscription, order by SubscriptionEventType;
- break;
- }
-
- // Two identical events for the same subscription in the same day, trust createdDate
- if (eventOrder == 0) {
- final int compCreatedDate = (((DefaultSubscriptionEvent) event).getCreatedDate()).compareTo(((DefaultSubscriptionEvent) cur).getCreatedDate());
- if (compCreatedDate <= 0) {
- break;
- }
- }
- }
- }
- index++;
- }
- result.add(index, event);
- }
-
- private SubscriptionEvent toSubscriptionEvent(@Nullable final SubscriptionEvent prev, @Nullable final SubscriptionEvent next,
- final UUID entitlementId, final BlockingState in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
- final Product prevProduct;
- final Plan prevPlan;
- final PlanPhase prevPlanPhase;
- final PriceList prevPriceList;
- final BillingPeriod prevBillingPeriod;
- // Enforce prev = null for start events
- if (prev == null || SubscriptionEventType.START_ENTITLEMENT.equals(eventType) || SubscriptionEventType.START_BILLING.equals(eventType)) {
- prevProduct = null;
- prevPlan = null;
- prevPlanPhase = null;
- prevPriceList = null;
- prevBillingPeriod = null;
- } else {
- // We look for the next for the 'prev' meaning we we are headed to, but if this is null -- for example on cancellation we get the prev which gives the correct state.
- prevProduct = (prev.getNextProduct() != null ? prev.getNextProduct() : prev.getPrevProduct());
- prevPlan = (prev.getNextPlan() != null ? prev.getNextPlan() : prev.getPrevPlan());
- prevPlanPhase = (prev.getNextPhase() != null ? prev.getNextPhase() : prev.getPrevPhase());
- prevPriceList = (prev.getNextPriceList() != null ? prev.getNextPriceList() : prev.getPrevPriceList());
- prevBillingPeriod = (prev.getNextBillingPeriod() != null ? prev.getNextBillingPeriod() : prev.getPrevBillingPeriod());
- }
-
- final Product nextProduct;
- final Plan nextPlan;
- final PlanPhase nextPlanPhase;
- final PriceList nextPriceList;
- final BillingPeriod nextBillingPeriod;
- if (SubscriptionEventType.PAUSE_ENTITLEMENT.equals(eventType) || SubscriptionEventType.PAUSE_BILLING.equals(eventType) ||
- SubscriptionEventType.RESUME_ENTITLEMENT.equals(eventType) || SubscriptionEventType.RESUME_BILLING.equals(eventType) ||
- (SubscriptionEventType.SERVICE_STATE_CHANGE.equals(eventType) && (prev == null || (!SubscriptionEventType.STOP_ENTITLEMENT.equals(prev.getSubscriptionEventType()) && !SubscriptionEventType.STOP_BILLING.equals(prev.getSubscriptionEventType()))))) {
- // Enforce next = prev for pause/resume events as well as service changes
- nextProduct = prevProduct;
- nextPlan = prevPlan;
- nextPlanPhase = prevPlanPhase;
- nextPriceList = prevPriceList;
- nextBillingPeriod = prevBillingPeriod;
- } else if (next == null) {
- // Enforce next = null for stop events
- if (prev == null || SubscriptionEventType.STOP_ENTITLEMENT.equals(eventType) || SubscriptionEventType.STOP_BILLING.equals(eventType)) {
- nextProduct = null;
- nextPlan = null;
- nextPlanPhase = null;
- nextPriceList = null;
- nextBillingPeriod = null;
- } else {
- nextProduct = prev.getNextProduct();
- nextPlan = prev.getNextPlan();
- nextPlanPhase = prev.getNextPhase();
- nextPriceList = prev.getNextPriceList();
- nextBillingPeriod = prev.getNextBillingPeriod();
- }
- } else {
- nextProduct = next.getNextProduct();
- nextPlan = next.getNextPlan();
- nextPlanPhase = next.getNextPhase();
- nextPriceList = next.getNextPriceList();
- nextBillingPeriod = next.getNextBillingPeriod();
- }
-
- // See https://github.com/killbill/killbill/issues/135
- final String serviceName = getRealServiceNameForEntitlementOrExternalServiceName(in.getService(), eventType);
-
- return new DefaultSubscriptionEvent(in.getId(),
- entitlementId,
- in.getEffectiveDate(),
- in.getCreatedDate(),
- eventType,
- in.isBlockEntitlement(),
- in.isBlockBilling(),
- serviceName,
- in.getStateName(),
- prevProduct,
- prevPlan,
- prevPlanPhase,
- prevPriceList,
- prevBillingPeriod,
- nextProduct,
- nextPlan,
- nextPlanPhase,
- nextPriceList,
- nextBillingPeriod,
- in.getCreatedDate(),
- accountTimeZone);
- }
-
- private static String getRealServiceNameForEntitlementOrExternalServiceName(final String originalServiceName, final SubscriptionEventType eventType) {
- final String serviceName;
- if (DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(originalServiceName)) {
- serviceName = getServiceName(eventType);
- } else {
- serviceName = originalServiceName;
- }
- return serviceName;
- }
-
- private SubscriptionEvent toSubscriptionEvent(final SubscriptionBaseTransition in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
- return new DefaultSubscriptionEvent(in.getId(),
- in.getSubscriptionId(),
- in.getEffectiveTransitionTime(),
- in.getRequestedTransitionTime(),
- eventType,
- false,
- false,
- getServiceName(eventType),
- eventType.toString(),
- (in.getPreviousPlan() != null ? in.getPreviousPlan().getProduct() : null),
- in.getPreviousPlan(),
- in.getPreviousPhase(),
- in.getPreviousPriceList(),
- (in.getPreviousPlan() != null ? in.getPreviousPlan().getRecurringBillingPeriod() : null),
- (in.getNextPlan() != null ? in.getNextPlan().getProduct() : null),
- in.getNextPlan(),
- in.getNextPhase(),
- in.getNextPriceList(),
- (in.getNextPlan() != null ? in.getNextPlan().getRecurringBillingPeriod() : null),
- in.getCreatedDate(),
- accountTimeZone);
- }
-
- private static String getServiceName(final SubscriptionEventType type) {
- switch (type) {
- case START_BILLING:
- case PAUSE_BILLING:
- case RESUME_BILLING:
- case STOP_BILLING:
- return BILLING_SERVICE_NAME;
-
- case PHASE:
- case CHANGE:
- return ENT_BILLING_SERVICE_NAME;
-
- default:
- return DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME;
- }
- }
-
- private List<SubscriptionEventType> toEventTypes(final SubscriptionBaseTransitionType in) {
- switch (in) {
- case CREATE:
- return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT, SubscriptionEventType.START_BILLING);
- case TRANSFER:
- return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT, SubscriptionEventType.START_BILLING);
- case MIGRATE_ENTITLEMENT:
- return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT);
- case MIGRATE_BILLING:
- return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_BILLING);
- case CHANGE:
- return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.CHANGE);
- case CANCEL:
- return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.STOP_BILLING);
- case PHASE:
- return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.PHASE);
- /*
- * Those can be ignored:
- */
- // Marker event
- case UNCANCEL:
- // Junction billing events-- that info is part of blocking states, we will get outside of subscription base
- case START_BILLING_DISABLED:
- case END_BILLING_DISABLED:
- default:
- return ImmutableList.<SubscriptionEventType>of();
- }
+ this.events = SubscriptionEventOrdering.sortedCopy(entitlements, accountTimeZone);
}
@Override
@@ -622,129 +57,50 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
return events;
}
- //
- // Internal class to keep the state associated with each subscription
- //
- private static final class TargetState {
-
- private boolean isEntitlementStarted;
- private boolean isEntitlementStopped;
- private boolean isBillingStarted;
- private boolean isBillingStopped;
- private Map<String, BlockingState> perServiceBlockingState;
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("DefaultSubscriptionBundleTimeline{");
+ sb.append("accountId=").append(accountId);
+ sb.append(", bundleId=").append(bundleId);
+ sb.append(", externalKey='").append(externalKey).append('\'');
+ sb.append(", events=").append(events);
+ sb.append('}');
+ return sb.toString();
+ }
- public TargetState() {
- this.isEntitlementStarted = false;
- this.isEntitlementStopped = false;
- this.isBillingStarted = false;
- this.isBillingStopped = false;
- this.perServiceBlockingState = new HashMap<String, BlockingState>();
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
}
-
- public void setEntitlementStarted() {
- isEntitlementStarted = true;
+ if (o == null || getClass() != o.getClass()) {
+ return false;
}
- public void setEntitlementStopped() {
- isEntitlementStopped = true;
- }
+ final DefaultSubscriptionBundleTimeline that = (DefaultSubscriptionBundleTimeline) o;
- public void setBillingStarted() {
- isBillingStarted = true;
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+ return false;
}
-
- public void setBillingStopped() {
- isBillingStopped = true;
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+ return false;
}
-
- public void addEntitlementEvent(final SubscriptionEvent e) {
- final String serviceName = getRealServiceNameForEntitlementOrExternalServiceName(e.getServiceName(), e.getSubscriptionEventType());
- final BlockingState lastBlockingStateForService = perServiceBlockingState.get(serviceName);
-
- // Assume the event has no impact on changes - TODO this is wrong for SERVICE_STATE_CHANGE
- final boolean blockChange = lastBlockingStateForService != null && lastBlockingStateForService.isBlockChange();
- // For block entitlement or billing, override the previous state
- final boolean blockedEntitlement = e.isBlockedEntitlement();
- final boolean blockedBilling = e.isBlockedBilling();
-
- final BlockingState converted = new DefaultBlockingState(e.getEntitlementId(),
- BlockingStateType.SUBSCRIPTION,
- e.getServiceStateName(),
- serviceName,
- blockChange,
- blockedEntitlement,
- blockedBilling,
- ((DefaultSubscriptionEvent) e).getEffectiveDateTime());
- perServiceBlockingState.put(converted.getService(), converted);
+ if (events != null ? !events.equals(that.events) : that.events != null) {
+ return false;
}
-
- //
- // From the current state of that subscription, compute the effect of the new state based on the incoming blockingState event
- //
- private List<SubscriptionEventType> addStateAndReturnEventTypes(final BlockingState bs) {
- // Turn off isBlockedEntitlement and isBlockedBilling if there was not start event
- final BlockingState fixedBlockingState = new DefaultBlockingState(bs.getBlockedId(),
- bs.getType(),
- bs.getStateName(),
- bs.getService(),
- bs.isBlockChange(),
- (bs.isBlockEntitlement() && isEntitlementStarted && !isEntitlementStopped),
- (bs.isBlockBilling() && isBillingStarted && !isBillingStopped),
- bs.getEffectiveDate());
-
- final List<SubscriptionEventType> result = new ArrayList<SubscriptionEventType>(4);
- if (fixedBlockingState.getStateName().equals(DefaultEntitlementApi.ENT_STATE_CANCELLED)) {
- isEntitlementStopped = true;
- result.add(SubscriptionEventType.STOP_ENTITLEMENT);
- return result;
- }
-
- //
- // We look at the effect of the incoming event for the specific service, and then recompute the state after so we can compare if anything has changed
- // across all services
- //
- final BlockingAggregator stateBefore = getState();
- if (DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(fixedBlockingState.getService())) {
- // Some blocking states will be added as entitlement-service and billing-service via addEntitlementEvent
- // (see above). Because of it, we need to multiplex entitlement events here.
- // TODO - this is magic and fragile. We should revisit how we create this state machine.
- perServiceBlockingState.put(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME, fixedBlockingState);
- perServiceBlockingState.put(DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME, fixedBlockingState);
- } else {
- perServiceBlockingState.put(fixedBlockingState.getService(), fixedBlockingState);
- }
- final BlockingAggregator stateAfter = getState();
-
- final boolean shouldResumeEntitlement = isEntitlementStarted && !isEntitlementStopped && stateBefore.isBlockEntitlement() && !stateAfter.isBlockEntitlement();
- if (shouldResumeEntitlement) {
- result.add(SubscriptionEventType.RESUME_ENTITLEMENT);
- }
- final boolean shouldResumeBilling = isBillingStarted && !isBillingStopped && stateBefore.isBlockBilling() && !stateAfter.isBlockBilling();
- if (shouldResumeBilling) {
- result.add(SubscriptionEventType.RESUME_BILLING);
- }
-
- final boolean shouldBlockEntitlement = isEntitlementStarted && !isEntitlementStopped && !stateBefore.isBlockEntitlement() && stateAfter.isBlockEntitlement();
- if (shouldBlockEntitlement) {
- result.add(SubscriptionEventType.PAUSE_ENTITLEMENT);
- }
- final boolean shouldBlockBilling = isBillingStarted && !isBillingStopped && !stateBefore.isBlockBilling() && stateAfter.isBlockBilling();
- if (shouldBlockBilling) {
- result.add(SubscriptionEventType.PAUSE_BILLING);
- }
-
- if (!shouldResumeEntitlement && !shouldResumeBilling && !shouldBlockEntitlement && !shouldBlockBilling && !fixedBlockingState.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
- result.add(SubscriptionEventType.SERVICE_STATE_CHANGE);
- }
- return result;
+ if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
+ return false;
}
- private BlockingAggregator getState() {
- final DefaultBlockingAggregator aggrBefore = new DefaultBlockingAggregator();
- for (final BlockingState cur : perServiceBlockingState.values()) {
- aggrBefore.or(cur);
- }
- return aggrBefore;
- }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = accountId != null ? accountId.hashCode() : 0;
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+ result = 31 * result + (events != null ? events.hashCode() : 0);
+ return result;
}
}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementOrderingBase.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementOrderingBase.java
new file mode 100644
index 0000000..6643f92
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementOrderingBase.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.entitlement.api;
+
+import org.killbill.billing.entitlement.DefaultEntitlementService;
+
+public abstract class EntitlementOrderingBase {
+
+ public static final String BILLING_SERVICE_NAME = "billing-service";
+ public static final String ENT_BILLING_SERVICE_NAME = "entitlement+billing-service";
+
+ protected static String getRealServiceNameForEntitlementOrExternalServiceName(final String originalServiceName, final SubscriptionEventType eventType) {
+ final String serviceName;
+ if (DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(originalServiceName)) {
+ serviceName = getServiceName(eventType);
+ } else {
+ serviceName = originalServiceName;
+ }
+ return serviceName;
+ }
+
+ protected static String getServiceName(final SubscriptionEventType type) {
+ switch (type) {
+ case START_BILLING:
+ case PAUSE_BILLING:
+ case RESUME_BILLING:
+ case STOP_BILLING:
+ return BILLING_SERVICE_NAME;
+
+ case PHASE:
+ case CHANGE:
+ return ENT_BILLING_SERVICE_NAME;
+
+ default:
+ return DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME;
+ }
+ }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/SubscriptionEventOrdering.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/SubscriptionEventOrdering.java
new file mode 100644
index 0000000..5d793a7
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/SubscriptionEventOrdering.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.entitlement.api;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.joda.time.DateTimeZone;
+import org.killbill.billing.entitlement.DefaultEntitlementService;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+//
+// Compute all events based on blocking states events and base subscription events
+// Note that:
+// - base subscription events are already ordered for each Entitlement and so when we reorder at the bundle level we try not to break that initial ordering
+// - blocking state events occur at various level (account, bundle and subscription) so for higher level, we need to dispatch that on each subscription.
+//
+public class SubscriptionEventOrdering extends EntitlementOrderingBase {
+
+ @VisibleForTesting
+ static final SubscriptionEventOrdering INSTANCE = new SubscriptionEventOrdering();
+
+ private SubscriptionEventOrdering() {}
+
+ public static List<SubscriptionEvent> sortedCopy(final Entitlement entitlement, final DateTimeZone accountTimeZone) {
+ return sortedCopy(ImmutableList.<Entitlement>of(entitlement), accountTimeZone);
+ }
+
+ public static List<SubscriptionEvent> sortedCopy(final Iterable<Entitlement> entitlements, final DateTimeZone accountTimeZone) {
+ return INSTANCE.computeEvents(entitlements, accountTimeZone);
+ }
+
+ private List<SubscriptionEvent> computeEvents(final Iterable<Entitlement> entitlements, final DateTimeZone accountTimeZone) {
+ // Compute base events across all entitlements (already ordered per entitlement)
+ final LinkedList<SubscriptionEvent> result = computeSubscriptionBaseEvents(entitlements, accountTimeZone);
+
+ // Add blocking states at the right place
+ BlockingStateOrdering.insertSorted(entitlements, accountTimeZone, result);
+
+ // Final cleanups
+ reOrderSubscriptionEventsOnSameDateByType(result);
+ removeOverlappingSubscriptionEvents(result);
+
+ return result;
+ }
+
+ // Compute the initial stream of events based on the subscription base events
+ private LinkedList<SubscriptionEvent> computeSubscriptionBaseEvents(final Iterable<Entitlement> entitlements, final DateTimeZone accountTimeZone) {
+ final LinkedList<SubscriptionEvent> result = new LinkedList<SubscriptionEvent>();
+ for (final Entitlement cur : entitlements) {
+ Preconditions.checkState(cur instanceof DefaultEntitlement, "Entitlement %s is not a DefaultEntitlement", cur);
+ final SubscriptionBase base = ((DefaultEntitlement) cur).getSubscriptionBase();
+ final List<SubscriptionBaseTransition> baseTransitions = base.getAllTransitions();
+ for (final SubscriptionBaseTransition tr : baseTransitions) {
+ final List<SubscriptionEventType> eventTypes = toEventTypes(tr.getTransitionType());
+ for (final SubscriptionEventType eventType : eventTypes) {
+ final SubscriptionEvent event = toSubscriptionEvent(tr, eventType, accountTimeZone);
+ insertSubscriptionEvent(event, result);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private List<SubscriptionEventType> toEventTypes(final SubscriptionBaseTransitionType in) {
+ switch (in) {
+ case CREATE:
+ return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT, SubscriptionEventType.START_BILLING);
+ case TRANSFER:
+ return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT, SubscriptionEventType.START_BILLING);
+ case MIGRATE_ENTITLEMENT:
+ return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT);
+ case MIGRATE_BILLING:
+ return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_BILLING);
+ case CHANGE:
+ return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.CHANGE);
+ case CANCEL:
+ return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.STOP_BILLING);
+ case PHASE:
+ return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.PHASE);
+ /*
+ * Those can be ignored:
+ */
+ // Marker event
+ case UNCANCEL:
+ // Junction billing events-- that info is part of blocking states, we will get outside of subscription base
+ case START_BILLING_DISABLED:
+ case END_BILLING_DISABLED:
+ default:
+ return ImmutableList.<SubscriptionEventType>of();
+ }
+ }
+
+ private void insertSubscriptionEvent(final SubscriptionEvent event, final List<SubscriptionEvent> result) {
+ int index = 0;
+ for (final SubscriptionEvent cur : result) {
+ final int compEffectiveDate = event.getEffectiveDate().compareTo(cur.getEffectiveDate());
+ if (compEffectiveDate < 0) {
+ // EffectiveDate is less than cur -> insert here
+ break;
+ } else if (compEffectiveDate == 0) {
+ final int compUUID = event.getEntitlementId().compareTo(cur.getEntitlementId());
+ if (compUUID < 0) {
+ // Same EffectiveDate but subscription are different, no need top sort further just return something deterministic
+ break;
+ } else if (compUUID == 0) {
+ final int eventOrder = event.getSubscriptionEventType().ordinal() - cur.getSubscriptionEventType().ordinal();
+ if (eventOrder < 0) {
+ // Same EffectiveDate and same subscription, order by SubscriptionEventType;
+ break;
+ }
+
+ // Two identical events for the same subscription in the same day, trust createdDate
+ if (eventOrder == 0) {
+ final int compCreatedDate = (((DefaultSubscriptionEvent) event).getCreatedDate()).compareTo(((DefaultSubscriptionEvent) cur).getCreatedDate());
+ if (compCreatedDate <= 0) {
+ break;
+ }
+ }
+ }
+ }
+ index++;
+ }
+ result.add(index, event);
+ }
+
+ private SubscriptionEvent toSubscriptionEvent(final SubscriptionBaseTransition in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
+ return new DefaultSubscriptionEvent(in.getId(),
+ in.getSubscriptionId(),
+ in.getEffectiveTransitionTime(),
+ in.getRequestedTransitionTime(),
+ eventType,
+ false,
+ false,
+ getServiceName(eventType),
+ eventType.toString(),
+ (in.getPreviousPlan() != null ? in.getPreviousPlan().getProduct() : null),
+ in.getPreviousPlan(),
+ in.getPreviousPhase(),
+ in.getPreviousPriceList(),
+ (in.getPreviousPlan() != null ? in.getPreviousPlan().getRecurringBillingPeriod() : null),
+ (in.getNextPlan() != null ? in.getNextPlan().getProduct() : null),
+ in.getNextPlan(),
+ in.getNextPhase(),
+ in.getNextPriceList(),
+ (in.getNextPlan() != null ? in.getNextPlan().getRecurringBillingPeriod() : null),
+ in.getCreatedDate(),
+ accountTimeZone);
+ }
+
+ //
+ // All events have been inserted and should be at the right place, except that we want to ensure that events for a given subscription,
+ // and for a given time are ordered by SubscriptionEventType.
+ //
+ // All this seems a little over complicated, and one wonders why we don't just shove all events and call Collections.sort on the list prior
+ // to return:
+ // - One explanation is that we don't know the events in advance and each time the new events to be inserted are computed from the current state
+ // of the stream, which requires ordering all along
+ // - A careful reader will notice that the algorithm is N^2, -- so that we care so much considering we have very events -- but in addition to that
+ // the recursive path will be used very infrequently and when it is used, this will be probably just reorder with the prev event and that's it.
+ //
+ @VisibleForTesting
+ protected void reOrderSubscriptionEventsOnSameDateByType(final List<SubscriptionEvent> events) {
+ final int size = events.size();
+ for (int i = 0; i < size; i++) {
+ final SubscriptionEvent cur = events.get(i);
+ final SubscriptionEvent next = (i < (size - 1)) ? events.get(i + 1) : null;
+
+ final boolean shouldSwap = (next != null && shouldSwap(cur, next, true));
+ final boolean shouldReverseSort = (next == null || shouldSwap);
+
+ int currentIndex = i;
+ if (shouldSwap) {
+ Collections.swap(events, i, i + 1);
+ }
+ if (shouldReverseSort) {
+ while (currentIndex >= 1) {
+ final SubscriptionEvent revCur = events.get(currentIndex);
+ final SubscriptionEvent other = events.get(currentIndex - 1);
+ if (shouldSwap(revCur, other, false)) {
+ Collections.swap(events, currentIndex, currentIndex - 1);
+ }
+ if (revCur.getEffectiveDate().compareTo(other.getEffectiveDate()) != 0) {
+ break;
+ }
+ currentIndex--;
+ }
+ }
+ }
+ }
+
+ // Make sure the argument supports the remove operation - hence expect a LinkedList, not a List
+ private void removeOverlappingSubscriptionEvents(final LinkedList<SubscriptionEvent> events) {
+ final Iterator<SubscriptionEvent> iterator = events.iterator();
+ final Map<String, DefaultSubscriptionEvent> prevPerService = new HashMap<String, DefaultSubscriptionEvent>();
+ while (iterator.hasNext()) {
+ final DefaultSubscriptionEvent current = (DefaultSubscriptionEvent) iterator.next();
+ final DefaultSubscriptionEvent prev = prevPerService.get(current.getServiceName());
+ if (prev != null) {
+ if (current.overlaps(prev)) {
+ iterator.remove();
+ } else {
+ prevPerService.put(current.getServiceName(), current);
+ }
+ } else {
+ prevPerService.put(current.getServiceName(), current);
+ }
+ }
+ }
+
+ private boolean shouldSwap(final SubscriptionEvent cur, final SubscriptionEvent other, final boolean isAscending) {
+ // For a given date, order by subscriptionId, and within subscription by event type
+ final int idComp = cur.getEntitlementId().compareTo(other.getEntitlementId());
+ final Integer comparison = compareSubscriptionEventsForSameEffectiveDateAndEntitlementId(cur, other);
+ return (cur.getEffectiveDate().compareTo(other.getEffectiveDate()) == 0 &&
+ ((isAscending &&
+ ((idComp > 0) ||
+ (idComp == 0 && comparison != null && comparison > 0))) ||
+ (!isAscending &&
+ ((idComp < 0) ||
+ (idComp == 0 && comparison != null && comparison < 0)))));
+ }
+
+ private Integer compareSubscriptionEventsForSameEffectiveDateAndEntitlementId(final SubscriptionEvent first, final SubscriptionEvent second) {
+ // For consistency, make sure entitlement-service and billing-service events always happen in a
+ // deterministic order (e.g. after other services for STOP events and before for START events)
+ if ((DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(first.getServiceName()) ||
+ BILLING_SERVICE_NAME.equals(first.getServiceName())) &&
+ !(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(second.getServiceName()) ||
+ BILLING_SERVICE_NAME.equals(second.getServiceName()))) {
+ // first is an entitlement-service or billing-service event, but not second
+ if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT) ||
+ first.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING) ||
+ first.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_ENTITLEMENT) ||
+ first.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_BILLING) ||
+ first.getSubscriptionEventType().equals(SubscriptionEventType.PHASE) ||
+ first.getSubscriptionEventType().equals(SubscriptionEventType.CHANGE)) {
+ return -1;
+ } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_ENTITLEMENT) ||
+ first.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_BILLING) ||
+ first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT) ||
+ first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
+ return 1;
+ } else {
+ // Default behavior
+ return -1;
+ }
+ } else if ((DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(second.getServiceName()) ||
+ BILLING_SERVICE_NAME.equals(second.getServiceName())) &&
+ !(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(first.getServiceName()) ||
+ BILLING_SERVICE_NAME.equals(first.getServiceName()))) {
+ // second is an entitlement-service or billing-service event, but not first
+ if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT) ||
+ second.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING) ||
+ second.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_ENTITLEMENT) ||
+ second.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_BILLING) ||
+ second.getSubscriptionEventType().equals(SubscriptionEventType.PHASE) ||
+ second.getSubscriptionEventType().equals(SubscriptionEventType.CHANGE)) {
+ return 1;
+ } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_ENTITLEMENT) ||
+ second.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_BILLING) ||
+ second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT) ||
+ second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
+ return -1;
+ } else {
+ // Default behavior
+ return 1;
+ }
+ } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT)) {
+ // START_ENTITLEMENT is always first
+ return -1;
+ } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT)) {
+ // START_ENTITLEMENT is always first
+ return 1;
+ } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
+ // STOP_BILLING is always last
+ return 1;
+ } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
+ // STOP_BILLING is always last
+ return -1;
+ } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING)) {
+ // START_BILLING is first after START_ENTITLEMENT
+ return -1;
+ } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING)) {
+ // START_BILLING is first after START_ENTITLEMENT
+ return 1;
+ } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT)) {
+ // STOP_ENTITLEMENT is last after STOP_BILLING
+ return 1;
+ } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT)) {
+ // STOP_ENTITLEMENT is last after STOP_BILLING
+ return -1;
+ } else {
+ // Trust the current ordering
+ return null;
+ }
+ }
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
index 337ad3b..b9f5c6a 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
@@ -17,18 +17,13 @@
package org.killbill.billing.entitlement.api;
import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Collection;
import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
-import org.mockito.Mockito;
-import org.testng.Assert;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
@@ -36,12 +31,17 @@ import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.entitlement.DefaultEntitlementService;
import org.killbill.billing.entitlement.EntitlementTestSuiteNoDB;
+import org.killbill.billing.entitlement.EventsStream;
import org.killbill.billing.junction.DefaultBlockingState;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
import org.killbill.billing.subscription.events.user.ApiEventType;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
@@ -60,8 +60,8 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
public class TestSubscriptionBundleTimeline extends DefaultSubscriptionBundleTimeline {
- public TestSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final List<Entitlement> entitlements, final List<BlockingState> allBlockingStates) {
- super(accountTimeZone, accountId, bundleId, externalKey, entitlements, allBlockingStates);
+ public TestSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final Iterable<Entitlement> entitlements) {
+ super(accountTimeZone, accountId, bundleId, externalKey, entitlements);
}
public SubscriptionEvent createEvent(final UUID subscriptionId, final SubscriptionEventType type, final DateTime effectiveDate) {
@@ -92,7 +92,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
@Test(groups = "fast")
public void testReOrderSubscriptionEventsOnInvalidOrder1() {
- final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+ final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
final UUID subscriptionId = UUID.randomUUID();
@@ -102,7 +102,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
- timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+ SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -112,7 +112,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
@Test(groups = "fast")
public void testReOrderSubscriptionEventsOnInvalidOrder2() {
- final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+ final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
final UUID subscriptionId = UUID.randomUUID();
@@ -122,7 +122,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
- timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+ SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -132,7 +132,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
@Test(groups = "fast")
public void testReOrderSubscriptionEventsOnInvalidOrder3() {
- final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+ final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
final UUID subscriptionId = UUID.randomUUID();
@@ -142,7 +142,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_ENTITLEMENT, effectiveDate));
- timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+ SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -152,7 +152,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
@Test(groups = "fast")
public void testReOrderSubscriptionEventsOnInvalidOrderAndDifferentSubscriptionsSameDates1() {
- final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+ final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
final UUID subscriptionId = UUID.fromString("60b64e0c-cefd-48c3-8de9-c731a9558165");
@@ -166,7 +166,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
- timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+ SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
Assert.assertEquals(events.get(0).getEntitlementId(), otherSubscriptionId);
@@ -179,7 +179,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
@Test(groups = "fast")
public void testReOrderSubscriptionEventsOnInvalidOrderAndDifferentSubscriptionsSameDates2() {
- final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+ final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
final UUID subscriptionId = UUID.fromString("35b3b340-31b2-46ea-b062-e9fc9fab3bc9");
@@ -193,7 +193,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
- timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+ SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -206,7 +206,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
@Test(groups = "fast")
public void testReOrderSubscriptionEventsOnInvalidOrderAndDifferentSubscriptionsDates() {
- final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+ final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
final UUID subscriptionId = UUID.randomUUID();
@@ -227,7 +227,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.PAUSE_ENTITLEMENT, otherEffectiveDate));
events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.PAUSE_BILLING, otherEffectiveDate));
- timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+ SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -244,7 +244,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
@Test(groups = "fast")
public void testReOrderSubscriptionEventsOnCorrectOrder() {
- final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+ final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
final UUID subscriptionId = UUID.randomUUID();
@@ -254,7 +254,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
- timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+ SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -294,7 +294,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
entitlements.add(entitlement);
- final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, Collections.<BlockingState>emptyList());
+ final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
assertEquals(timeline.getAccountId(), accountId);
assertEquals(timeline.getBundleId(), bundleId);
@@ -314,9 +314,9 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
- assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
- assertEquals(events.get(3).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
+ assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
+ assertEquals(events.get(3).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertNull(events.get(0).getPrevPhase());
assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -362,10 +362,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
allTransitions.add(tr2);
final List<Entitlement> entitlements = new ArrayList<Entitlement>();
- final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+ final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
entitlements.add(entitlement);
- final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+ final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
assertEquals(timeline.getAccountId(), accountId);
assertEquals(timeline.getBundleId(), bundleId);
@@ -385,9 +385,9 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertEquals(events.get(2).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(3).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(3).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertNull(events.get(0).getPrevPhase());
assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -457,10 +457,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
blockingStates.add(bs4);
final List<Entitlement> entitlements = new ArrayList<Entitlement>();
- final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+ final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
entitlements.add(entitlement);
- final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+ final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
assertEquals(timeline.getAccountId(), accountId);
assertEquals(timeline.getBundleId(), bundleId);
@@ -493,14 +493,14 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(8).getSubscriptionEventType(), SubscriptionEventType.RESUME_BILLING);
assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
- assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+ assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
assertEquals(events.get(3).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(4).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(4).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertEquals(events.get(5).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(6).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(6).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertEquals(events.get(7).getServiceName(), service);
assertEquals(events.get(8).getServiceName(), service);
@@ -596,10 +596,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
allTransitions.add(tr3);
final List<Entitlement> entitlements = new ArrayList<Entitlement>();
- final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+ final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
entitlements.add(entitlement);
- final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+ final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
assertEquals(timeline.getAccountId(), accountId);
assertEquals(timeline.getBundleId(), bundleId);
@@ -636,9 +636,9 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(9).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
- assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+ assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
assertEquals(events.get(3).getServiceName(), overdueService);
assertEquals(events.get(4).getServiceName(), overdueService);
@@ -648,7 +648,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(7).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
assertEquals(events.get(8).getServiceName(), overdueService);
- assertEquals(events.get(9).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(9).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertNull(events.get(0).getPrevPhase());
assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -720,10 +720,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
allTransitions.add(tr3);
final List<Entitlement> entitlements = new ArrayList<Entitlement>();
- final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+ final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
entitlements.add(entitlement);
- final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+ final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
assertEquals(timeline.getAccountId(), accountId);
assertEquals(timeline.getBundleId(), bundleId);
@@ -745,10 +745,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
- assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+ assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
+ assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
assertEquals(events.get(3).getServiceName(), service);
- assertEquals(events.get(4).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(4).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertNull(events.get(0).getPrevPhase());
assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -804,10 +804,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
blockingStates.add(bs2);
final List<Entitlement> entitlements = new ArrayList<Entitlement>();
- final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+ final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
entitlements.add(entitlement);
- final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+ final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
assertEquals(events.size(), 6);
@@ -827,11 +827,11 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
- assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+ assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
+ assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
assertEquals(events.get(3).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
assertEquals(events.get(4).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(5).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(5).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertNull(events.get(0).getPrevPhase());
assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -899,13 +899,13 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
blockingStates.add(bs2);
final List<Entitlement> entitlements = new ArrayList<Entitlement>();
- final Entitlement entitlement1 = createEntitlement(entitlementId1, allTransitions1);
+ final Entitlement entitlement1 = createEntitlement(entitlementId1, allTransitions1, blockingStates);
entitlements.add(entitlement1);
- final Entitlement entitlement2 = createEntitlement(entitlementId2, allTransitions2);
+ final Entitlement entitlement2 = createEntitlement(entitlementId2, allTransitions2, blockingStates);
entitlements.add(entitlement2);
- final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+ final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
assertEquals(events.size(), 9);
@@ -937,17 +937,17 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(8).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertEquals(events.get(2).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(3).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(3).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
- assertEquals(events.get(4).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+ assertEquals(events.get(4).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
assertEquals(events.get(5).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
assertEquals(events.get(6).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
assertEquals(events.get(7).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(8).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(8).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertNull(events.get(0).getPrevPhase());
assertEquals(events.get(0).getNextPhase().getName(), "trial1");
@@ -1006,8 +1006,8 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
allTransitions.add(tr3);
// Verify the timeline without the blocking state events
- final ImmutableList<Entitlement> entitlementsWithoutBlockingStates = ImmutableList.<Entitlement>of(createEntitlement(entitlementId, allTransitions));
- final List<SubscriptionEvent> eventsWithoutBlockingStates = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlementsWithoutBlockingStates, blockingStates).getSubscriptionEvents();
+ final ImmutableList<Entitlement> entitlementsWithoutBlockingStates = ImmutableList.<Entitlement>of(createEntitlement(entitlementId, allTransitions, blockingStates));
+ final List<SubscriptionEvent> eventsWithoutBlockingStates = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlementsWithoutBlockingStates).getSubscriptionEvents();
assertEquals(eventsWithoutBlockingStates.size(), 4);
assertEquals(eventsWithoutBlockingStates.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
assertEquals(eventsWithoutBlockingStates.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -1022,8 +1022,8 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
blockingStates.add(bs1);
// Verify the timeline with the overdue event blocking the entitlement
- final ImmutableList<Entitlement> entitlementsWithOverdueEvent = ImmutableList.<Entitlement>of(createEntitlement(entitlementId, allTransitions));
- final List<SubscriptionEvent> eventsWithOverdueEvent = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlementsWithOverdueEvent, blockingStates).getSubscriptionEvents();
+ final ImmutableList<Entitlement> entitlementsWithOverdueEvent = ImmutableList.<Entitlement>of(createEntitlement(entitlementId, allTransitions, blockingStates));
+ final List<SubscriptionEvent> eventsWithOverdueEvent = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlementsWithOverdueEvent).getSubscriptionEvents();
assertEquals(eventsWithOverdueEvent.size(), 5);
assertEquals(eventsWithOverdueEvent.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
assertEquals(eventsWithOverdueEvent.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -1038,11 +1038,11 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
blockingStates.add(bs2);
final List<Entitlement> entitlements = new ArrayList<Entitlement>();
- final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+ final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
entitlements.add(entitlement);
// Verify the timeline with both the overdue event and the entitlement cancel event
- final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+ final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
assertEquals(timeline.getAccountId(), accountId);
assertEquals(timeline.getBundleId(), bundleId);
@@ -1066,11 +1066,11 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
- assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+ assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
+ assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
assertEquals(events.get(3).getServiceName(), service);
assertEquals(events.get(4).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(5).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(5).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertNull(events.get(0).getPrevPhase());
assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -1139,10 +1139,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
blockingStates.add(bs4);
final List<Entitlement> entitlements = new ArrayList<Entitlement>();
- final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+ final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
entitlements.add(entitlement);
- final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+ final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
assertEquals(events.size(), 4);
@@ -1158,7 +1158,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertEquals(events.get(2).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
assertEquals(events.get(3).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
@@ -1232,10 +1232,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
blockingStates.add(bs5);
final List<Entitlement> entitlements = new ArrayList<Entitlement>();
- final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+ final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
entitlements.add(entitlement);
- final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+ final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
assertEquals(events.size(), 11);
@@ -1270,17 +1270,17 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(10).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertEquals(events.get(2).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(3).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(3).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertEquals(events.get(4).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(5).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(5).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertEquals(events.get(6).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(7).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(7).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertEquals(events.get(8).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
- assertEquals(events.get(9).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+ assertEquals(events.get(9).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
assertEquals(events.get(10).getServiceName(), overdueService);
@@ -1312,9 +1312,17 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
}
private Entitlement createEntitlement(final UUID entitlementId, final List<SubscriptionBaseTransition> allTransitions) {
+ return createEntitlement(entitlementId, allTransitions, ImmutableList.<BlockingState>of());
+ }
+
+ private Entitlement createEntitlement(final UUID entitlementId, final List<SubscriptionBaseTransition> allTransitions, final Collection<BlockingState> blockingStates) {
final DefaultEntitlement result = Mockito.mock(DefaultEntitlement.class);
Mockito.when(result.getId()).thenReturn(entitlementId);
+ final EventsStream eventsStream = Mockito.mock(EventsStream.class);
+ Mockito.when(eventsStream.getBlockingStates()).thenReturn(blockingStates);
+ Mockito.when(result.getEventsStream()).thenReturn(eventsStream);
+
final SubscriptionBase base = Mockito.mock(SubscriptionBase.class);
Mockito.when(base.getAllTransitions()).thenReturn(allTransitions);
Mockito.when(result.getSubscriptionBase()).thenReturn(base);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
index 9c3a1f0..22effcd 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
*
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
*
@@ -23,39 +25,49 @@ import javax.annotation.Nullable;
import org.killbill.billing.entitlement.api.Subscription;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
-import org.killbill.billing.entitlement.api.SubscriptionEvent;
import org.killbill.billing.util.audit.AccountAuditLogs;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
import com.wordnik.swagger.annotations.ApiModelProperty;
public class BundleJson extends JsonBase {
@ApiModelProperty(dataType = "java.util.UUID", required = true)
- protected final String accountId;
+ private final String accountId;
@ApiModelProperty(dataType = "java.util.UUID")
- protected final String bundleId;
- protected final String externalKey;
+ private final String bundleId;
+ private final String externalKey;
private final List<SubscriptionJson> subscriptions;
+ private final BundleTimelineJson timeline;
@JsonCreator
public BundleJson(@JsonProperty("accountId") @Nullable final String accountId,
@JsonProperty("bundleId") @Nullable final String bundleId,
@JsonProperty("externalKey") @Nullable final String externalKey,
@JsonProperty("subscriptions") @Nullable final List<SubscriptionJson> subscriptions,
+ @JsonProperty("timeline") @Nullable final BundleTimelineJson timeline,
@JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
super(auditLogs);
this.accountId = accountId;
this.bundleId = bundleId;
this.externalKey = externalKey;
this.subscriptions = subscriptions;
+ this.timeline = timeline;
+ }
+
+ public BundleJson(final SubscriptionBundle bundle, @Nullable final AccountAuditLogs accountAuditLogs) {
+ super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForBundle(bundle.getId())));
+ this.accountId = bundle.getAccountId().toString();
+ this.bundleId = bundle.getId().toString();
+ this.externalKey = bundle.getExternalKey();
+ this.subscriptions = new LinkedList<SubscriptionJson>();
+ for (final Subscription subscription : bundle.getSubscriptions()) {
+ this.subscriptions.add(new SubscriptionJson(subscription, accountAuditLogs));
+ }
+ this.timeline = new BundleTimelineJson(bundle.getTimeline(), accountAuditLogs);
}
- @JsonProperty("subscriptions")
public List<SubscriptionJson> getSubscriptions() {
return subscriptions;
}
@@ -72,32 +84,20 @@ public class BundleJson extends JsonBase {
return externalKey;
}
- public BundleJson(final SubscriptionBundle bundle, @Nullable final AccountAuditLogs accountAuditLogs) {
- super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForBundle(bundle.getId())));
- this.accountId = bundle.getAccountId().toString();
- this.bundleId = bundle.getId().toString();
- this.externalKey = bundle.getExternalKey();
-
- this.subscriptions = new LinkedList<SubscriptionJson>();
- for (final Subscription cur : bundle.getSubscriptions()) {
- final ImmutableList<SubscriptionEvent> events = ImmutableList.<SubscriptionEvent>copyOf(Collections2.filter(bundle.getTimeline().getSubscriptionEvents(), new Predicate<SubscriptionEvent>() {
- @Override
- public boolean apply(@Nullable final SubscriptionEvent input) {
- return input.getEntitlementId().equals(cur.getId());
- }
- }));
- this.subscriptions.add(new SubscriptionJson(cur, events, accountAuditLogs));
- }
+ public BundleTimelineJson getTimeline() {
+ return timeline;
}
@Override
public String toString() {
- return "BundleJson{" +
- "accountId='" + accountId + '\'' +
- ", bundleId='" + bundleId + '\'' +
- ", externalKey='" + externalKey + '\'' +
- ", subscriptions=" + subscriptions +
- '}';
+ final StringBuilder sb = new StringBuilder("BundleJson{");
+ sb.append("accountId='").append(accountId).append('\'');
+ sb.append(", bundleId='").append(bundleId).append('\'');
+ sb.append(", externalKey='").append(externalKey).append('\'');
+ sb.append(", subscriptions=").append(subscriptions);
+ sb.append(", timeline=").append(timeline);
+ sb.append('}');
+ return sb.toString();
}
@Override
@@ -123,6 +123,9 @@ public class BundleJson extends JsonBase {
if (subscriptions != null ? !subscriptions.equals(that.subscriptions) : that.subscriptions != null) {
return false;
}
+ if (timeline != null ? !timeline.equals(that.timeline) : that.timeline != null) {
+ return false;
+ }
return true;
}
@@ -133,6 +136,7 @@ public class BundleJson extends JsonBase {
result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
result = 31 * result + (subscriptions != null ? subscriptions.hashCode() : 0);
+ result = 31 * result + (timeline != null ? timeline.hashCode() : 0);
return result;
}
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java
index 942daee..9dac4cb 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java
@@ -16,55 +16,79 @@
package org.killbill.billing.jaxrs.json;
+import java.util.LinkedList;
import java.util.List;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public class BundleTimelineJson {
+import javax.annotation.Nullable;
- private final String viewId;
+import org.killbill.billing.entitlement.api.SubscriptionBundleTimeline;
+import org.killbill.billing.entitlement.api.SubscriptionEvent;
+import org.killbill.billing.jaxrs.json.SubscriptionJson.EventSubscriptionJson;
+import org.killbill.billing.util.audit.AccountAuditLogs;
- private final BundleJson bundle;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.wordnik.swagger.annotations.ApiModelProperty;
- private final List<InvoicePaymentJson> payments;
+public class BundleTimelineJson extends JsonBase {
- private final List<InvoiceJson> invoices;
+ @ApiModelProperty(dataType = "java.util.UUID")
+ private final String accountId;
+ @ApiModelProperty(dataType = "java.util.UUID")
+ private final String bundleId;
+ private final String externalKey;
+ private final List<EventSubscriptionJson> events;
+ @JsonCreator
+ public BundleTimelineJson(@JsonProperty("accountId") @Nullable final String accountId,
+ @JsonProperty("bundleId") @Nullable final String bundleId,
+ @JsonProperty("externalKey") @Nullable final String externalKey,
+ @JsonProperty("events") @Nullable final List<EventSubscriptionJson> events,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
+ this.accountId = accountId;
+ this.bundleId = bundleId;
+ this.externalKey = externalKey;
+ this.events = events;
+ }
- private final String reasonForChange;
+ public BundleTimelineJson(final SubscriptionBundleTimeline bundleTimeline, @Nullable final AccountAuditLogs accountAuditLogs) {
+ super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForBundle(bundleTimeline.getBundleId())));
+ this.accountId = bundleTimeline.getAccountId().toString();
+ this.bundleId = bundleTimeline.getBundleId().toString();
+ this.externalKey = bundleTimeline.getExternalKey();
- @JsonCreator
- public BundleTimelineJson(@JsonProperty("viewId") final String viewId,
- @JsonProperty("bundle") final BundleJson bundle,
- @JsonProperty("payments") final List<InvoicePaymentJson> payments,
- @JsonProperty("invoices") final List<InvoiceJson> invoices,
- @JsonProperty("reasonForChange") final String reason) {
- this.viewId = viewId;
- this.bundle = bundle;
- this.payments = payments;
- this.invoices = invoices;
- this.reasonForChange = reason;
+ this.events = new LinkedList<EventSubscriptionJson>();
+ for (final SubscriptionEvent subscriptionEvent : bundleTimeline.getSubscriptionEvents()) {
+ this.events.add(new EventSubscriptionJson(subscriptionEvent, accountAuditLogs));
+ }
}
- public String getViewId() {
- return viewId;
+ public String getAccountId() {
+ return accountId;
}
- public BundleJson getBundle() {
- return bundle;
+ public String getBundleId() {
+ return bundleId;
}
- public List<InvoicePaymentJson> getPayments() {
- return payments;
+ public String getExternalKey() {
+ return externalKey;
}
- public List<InvoiceJson> getInvoices() {
- return invoices;
+ public List<EventSubscriptionJson> getEvents() {
+ return events;
}
- public String getReasonForChange() {
- return reasonForChange;
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("BundleTimelineJson{");
+ sb.append("accountId='").append(accountId).append('\'');
+ sb.append(", bundleId='").append(bundleId).append('\'');
+ sb.append(", externalKey='").append(externalKey).append('\'');
+ sb.append(", events=").append(events);
+ sb.append('}');
+ return sb.toString();
}
@Override
@@ -78,19 +102,16 @@ public class BundleTimelineJson {
final BundleTimelineJson that = (BundleTimelineJson) o;
- if (bundle != null ? !bundle.equals(that.bundle) : that.bundle != null) {
- return false;
- }
- if (invoices != null ? !invoices.equals(that.invoices) : that.invoices != null) {
+ if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
return false;
}
- if (payments != null ? !payments.equals(that.payments) : that.payments != null) {
+ if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
return false;
}
- if (reasonForChange != null ? !reasonForChange.equals(that.reasonForChange) : that.reasonForChange != null) {
+ if (events != null ? !events.equals(that.events) : that.events != null) {
return false;
}
- if (viewId != null ? !viewId.equals(that.viewId) : that.viewId != null) {
+ if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
return false;
}
@@ -99,11 +120,10 @@ public class BundleTimelineJson {
@Override
public int hashCode() {
- int result = viewId != null ? viewId.hashCode() : 0;
- result = 31 * result + (bundle != null ? bundle.hashCode() : 0);
- result = 31 * result + (payments != null ? payments.hashCode() : 0);
- result = 31 * result + (invoices != null ? invoices.hashCode() : 0);
- result = 31 * result + (reasonForChange != null ? reasonForChange.hashCode() : 0);
+ int result = accountId != null ? accountId.hashCode() : 0;
+ result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+ result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+ result = 31 * result + (events != null ? events.hashCode() : 0);
return result;
}
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
index 44aab8e..17e1f02 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
@@ -105,6 +105,26 @@ public class SubscriptionJson extends JsonBase {
this.phase = phase;
}
+ public EventSubscriptionJson(final SubscriptionEvent subscriptionEvent, @Nullable final AccountAuditLogs accountAuditLogs) {
+ super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForSubscriptionEvent(subscriptionEvent.getId())));
+ final BillingPeriod billingPeriod = subscriptionEvent.getNextBillingPeriod() != null ? subscriptionEvent.getNextBillingPeriod() : subscriptionEvent.getPrevBillingPeriod();
+ final Product product = subscriptionEvent.getNextProduct() != null ? subscriptionEvent.getNextProduct() : subscriptionEvent.getPrevProduct();
+ final PriceList priceList = subscriptionEvent.getNextPriceList() != null ? subscriptionEvent.getNextPriceList() : subscriptionEvent.getPrevPriceList();
+ final PlanPhase phase = subscriptionEvent.getNextPhase() != null ? subscriptionEvent.getNextPhase() : subscriptionEvent.getPrevPhase();
+ this.eventId = subscriptionEvent.getId().toString();
+ this.billingPeriod = billingPeriod != null ? billingPeriod.toString() : null;
+ this.requestedDate = subscriptionEvent.getRequestedDate();
+ this.effectiveDate = subscriptionEvent.getEffectiveDate();
+ this.product = product != null ? product.getName() : null;
+ this.priceList = priceList != null ? priceList.getName() : null;
+ this.eventType = subscriptionEvent.getSubscriptionEventType().toString();
+ this.isBlockedBilling = subscriptionEvent.isBlockedBilling();
+ this.isBlockedEntitlement = subscriptionEvent.isBlockedEntitlement();
+ this.serviceName = subscriptionEvent.getServiceName();
+ this.serviceStateName = subscriptionEvent.getServiceStateName();
+ this.phase = phase != null ? phase.getName() : null;
+ }
+
public String getEventId() {
return eventId;
}
@@ -274,9 +294,7 @@ public class SubscriptionJson extends JsonBase {
this.events = events;
}
- public SubscriptionJson(final Subscription subscription,
- final List<SubscriptionEvent> subscriptionEvents,
- @Nullable final AccountAuditLogs accountAuditLogs) {
+ public SubscriptionJson(final Subscription subscription, @Nullable final AccountAuditLogs accountAuditLogs) {
super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForSubscription(subscription.getId())));
this.startDate = subscription.getEffectiveStartDate();
this.productName = subscription.getLastActiveProduct().getName();
@@ -291,27 +309,9 @@ public class SubscriptionJson extends JsonBase {
this.bundleId = subscription.getBundleId().toString();
this.subscriptionId = subscription.getId().toString();
this.externalKey = subscription.getExternalKey();
- this.events = subscriptionEvents != null ? new LinkedList<EventSubscriptionJson>() : null;
- if (events != null) {
- for (final SubscriptionEvent cur : subscriptionEvents) {
- final BillingPeriod billingPeriod = cur.getNextBillingPeriod() != null ? cur.getNextBillingPeriod() : cur.getPrevBillingPeriod();
- final Product product = cur.getNextProduct() != null ? cur.getNextProduct() : cur.getPrevProduct();
- final PriceList priceList = cur.getNextPriceList() != null ? cur.getNextPriceList() : cur.getPrevPriceList();
- final PlanPhase phase = cur.getNextPhase() != null ? cur.getNextPhase() : cur.getPrevPhase();
- this.events.add(new EventSubscriptionJson(cur.getId().toString(),
- billingPeriod != null ? billingPeriod.toString() : null,
- cur.getRequestedDate(),
- cur.getEffectiveDate(),
- product != null ? product.getName() : null,
- priceList != null ? priceList.getName() : null,
- cur.getSubscriptionEventType().toString(),
- cur.isBlockedBilling(),
- cur.isBlockedEntitlement(),
- cur.getServiceName(),
- cur.getServiceStateName(),
- phase != null ? phase.getName() : null,
- toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForSubscriptionEvent(cur.getId()))));
- }
+ this.events = new LinkedList<EventSubscriptionJson>();
+ for (final SubscriptionEvent subscriptionEvent : subscription.getSubscriptionEvents()) {
+ this.events.add(new EventSubscriptionJson(subscriptionEvent, accountAuditLogs));
}
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index e83a503..f272f2c 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
*
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
*
@@ -127,7 +129,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
@javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
final UUID uuid = UUID.fromString(subscriptionId);
final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(uuid, context.createContext(request));
- final SubscriptionJson json = new SubscriptionJson(subscription, null, null);
+ final SubscriptionJson json = new SubscriptionJson(subscription, null);
return Response.status(Status.OK).entity(json).build();
}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
index 6cf55c8..933d6bd 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
@@ -70,7 +70,7 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuiteNoDB {
ImmutableList.<EventSubscriptionJson>of(event),
auditLogs);
- final BundleJson bundleJson = new BundleJson(someUUID, bundleId.toString(), externalKey, ImmutableList.<SubscriptionJson>of(subscription), auditLogs);
+ final BundleJson bundleJson = new BundleJson(someUUID, bundleId.toString(), externalKey, ImmutableList.<SubscriptionJson>of(subscription), null, auditLogs);
Assert.assertEquals(bundleJson.getBundleId(), bundleId.toString());
Assert.assertEquals(bundleJson.getExternalKey(), externalKey);
Assert.assertEquals(bundleJson.getSubscriptions().size(), 1);
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
index 28e6a46..9874b55 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
@@ -18,85 +18,42 @@
package org.killbill.billing.jaxrs.json;
-import java.math.BigDecimal;
import java.util.UUID;
-import org.joda.time.DateTime;
import org.joda.time.LocalDate;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+import org.killbill.billing.jaxrs.json.SubscriptionJson.EventSubscriptionJson;
import org.testng.Assert;
import org.testng.annotations.Test;
-import org.killbill.billing.catalog.api.Currency;
-import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
-
import com.google.common.collect.ImmutableList;
public class TestBundleTimelineJson extends JaxrsTestSuiteNoDB {
@Test(groups = "fast")
public void testJson() throws Exception {
- final String viewId = UUID.randomUUID().toString();
- final String reason = UUID.randomUUID().toString();
-
- final BundleJson bundleJson = createBundleWithSubscriptions();
- final InvoiceJson invoiceJson = createInvoice();
- final InvoicePaymentJson paymentJson = createPayment(UUID.fromString(invoiceJson.getAccountId()),
- UUID.fromString(invoiceJson.getInvoiceId()));
-
- final BundleTimelineJson bundleTimelineJson = new BundleTimelineJson(viewId,
- bundleJson,
- ImmutableList.<InvoicePaymentJson>of(paymentJson),
- ImmutableList.<InvoiceJson>of(invoiceJson),
- reason);
+ final EventSubscriptionJson event = new EventSubscriptionJson(UUID.randomUUID().toString(),
+ BillingPeriod.NO_BILLING_PERIOD.toString(),
+ new LocalDate(),
+ new LocalDate(),
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ true,
+ false,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ null);
+ final BundleTimelineJson bundleTimelineJson = new BundleTimelineJson(UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<EventSubscriptionJson>of(event),
+ null);
final String asJson = mapper.writeValueAsString(bundleTimelineJson);
final BundleTimelineJson fromJson = mapper.readValue(asJson, BundleTimelineJson.class);
Assert.assertEquals(fromJson, bundleTimelineJson);
}
-
- private BundleJson createBundleWithSubscriptions() {
- final String someUUID = UUID.randomUUID().toString();
- final UUID accountId = UUID.randomUUID();
- final UUID bundleId = UUID.randomUUID();
- final UUID subscriptionId = UUID.randomUUID();
- final String externalKey = UUID.randomUUID().toString();
-
- final SubscriptionJson entitlementJsonWithEvents = new SubscriptionJson(accountId.toString(), bundleId.toString(), subscriptionId.toString(), externalKey,
- new LocalDate(), someUUID, someUUID, someUUID, someUUID,
- new LocalDate(), new LocalDate(), new LocalDate(), new LocalDate(),
- null, null);
- return new BundleJson(accountId.toString(), bundleId.toString(), externalKey, ImmutableList.<SubscriptionJson>of(entitlementJsonWithEvents), null);
- }
-
- private InvoiceJson createInvoice() {
- final UUID accountId = UUID.randomUUID();
- final UUID invoiceId = UUID.randomUUID();
- final BigDecimal invoiceAmount = BigDecimal.TEN;
- final BigDecimal creditAdj = BigDecimal.ONE;
- final BigDecimal refundAdj = BigDecimal.ONE;
- final LocalDate invoiceDate = clock.getUTCToday();
- final LocalDate targetDate = clock.getUTCToday();
- final String invoiceNumber = UUID.randomUUID().toString();
- final BigDecimal balance = BigDecimal.ZERO;
-
- return new InvoiceJson(invoiceAmount, Currency.USD.toString(), creditAdj, refundAdj, invoiceId.toString(), invoiceDate,
- targetDate, invoiceNumber, balance, accountId.toString(), null, null, null, null);
- }
-
- private InvoicePaymentJson createPayment(final UUID accountId, final UUID invoiceId) {
- final UUID paymentId = UUID.randomUUID();
- final Integer paymentNumber = 17;
- final String paymentExternalKey = UUID.randomUUID().toString();
- final BigDecimal authAmount = BigDecimal.TEN;
- final BigDecimal captureAmount = BigDecimal.ZERO;
- final BigDecimal purchasedAMount = BigDecimal.ZERO;
- final BigDecimal creditAmount = BigDecimal.ZERO;
- final BigDecimal refundAmount = BigDecimal.ZERO;
- final String currency = "USD";
-
- return new InvoicePaymentJson(invoiceId.toString(), accountId.toString(), paymentId.toString(), paymentNumber.toString(),
- paymentExternalKey, authAmount, captureAmount, purchasedAMount, refundAmount, creditAmount, currency,
- UUID.randomUUID().toString(),
- null, null);
- }
}