DefaultSubscriptionBundleTimeline.java

574 lines | 24.961 kB Blame History Raw Download
/*
 * Copyright 2010-2013 Ning, Inc.
 *
 * Ning licenses this file to you under the Apache License, version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at:
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package com.ning.billing.entitlement.api;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;

import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.Plan;
import com.ning.billing.catalog.api.PlanPhase;
import com.ning.billing.catalog.api.PriceList;
import com.ning.billing.catalog.api.Product;
import com.ning.billing.entitlement.DefaultEntitlementService;
import com.ning.billing.subscription.api.SubscriptionBase;
import com.ning.billing.subscription.api.SubscriptionBaseTransitionType;
import com.ning.billing.subscription.api.user.SubscriptionBaseTransition;

import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;

public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTimeline {

    private final List<SubscriptionEvent> events;
    private final UUID accountId;
    private final UUID bundleId;
    private final String externalKey;


    public DefaultSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final List<Entitlement> entitlements, List<BlockingState> allBlockingStates) {
        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 List<Entitlement> entitlements, List<BlockingState> allBlockingStates, final DateTimeZone accountTimeZone) {

        // Extract ids for all entitlement in the list
        final Set<UUID> allEntitlementUUIDs = new TreeSet(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);

        // Order allBlockingStates  events by effectiveDate, createdDate, uuid, service, serviceState
        Collections.sort(allBlockingStates, new Comparator<BlockingState>() {
            @Override
            public int compare(final BlockingState o1, final BlockingState o2) {
                final int effectivedComp = o1.getEffectiveDate().compareTo(o2.getEffectiveDate());
                if (effectivedComp != 0) {
                    return effectivedComp;
                }
                final int createdDateComp = o1.getCreatedDate().compareTo(o2.getCreatedDate());
                if (createdDateComp != 0) {
                    return createdDateComp;
                }
                final int uuidComp = o1.getId().compareTo(o2.getId());
                if (uuidComp != 0) {
                    return uuidComp;
                }
                // Same effectiveDate, createdDate and for the same object, we sort first by serviceName and then serviceState
                final int serviceNameComp = o1.getService().compareTo(o2.getService());
                if (serviceNameComp != 0) {
                    return serviceNameComp;
                }
                final int serviceStateComp = o1.getStateName().compareTo(o2.getStateName());
                if (serviceStateComp != 0) {
                    return serviceStateComp;
                }
                // Underministic-- not sure that will ever happen.
                return 0;
            }
        });

        for (BlockingState bs : allBlockingStates) {
            final LocalDate bsEffectiveDate = new LocalDate(bs.getEffectiveDate(), accountTimeZone);


            final List<SubscriptionEvent> newEvents = new ArrayList<SubscriptionEvent>();
            int index = insertFromBlockingEvent(accountTimeZone, allEntitlementUUIDs, result, bs, bsEffectiveDate, newEvents);
            result.addAll(index, newEvents);
        }
        return result;
    }

    private int insertFromBlockingEvent(final DateTimeZone accountTimeZone, final Set<UUID> allEntitlementUUIDs, final LinkedList<SubscriptionEvent> result, final BlockingState bs, final LocalDate bsEffectiveDate, final List<SubscriptionEvent> newEvents) {


        // In the beginning there was nothing...
        final Map<UUID, Boolean> blockedEntitlementMap = new HashMap<UUID, Boolean>();
        final Map<UUID, Boolean> blockedBillingMap = new HashMap<UUID, Boolean>();
        for (UUID uuid : allEntitlementUUIDs) {
            blockedEntitlementMap.put(uuid, Boolean.TRUE);
            blockedBillingMap.put(uuid, Boolean.TRUE);
        }

        int index = -1;
        final Iterator<SubscriptionEvent> it = result.iterator();
        DefaultSubscriptionEvent cur = null;
        while (it.hasNext()) {
            cur = (DefaultSubscriptionEvent) it.next();
            index++;

            final int compEffectiveDate = bsEffectiveDate.compareTo(cur.getEffectiveDate());
            final boolean shouldContinue = (compEffectiveDate > 0 ||
                                            (compEffectiveDate == 0 && bs.getCreatedDate().compareTo(cur.getCreatedDate()) >= 0));

            if (shouldContinue) {
                switch (cur.getSubscriptionEventType()) {
                    case START_ENTITLEMENT:
                        blockedEntitlementMap.put(cur.getEntitlementId(), Boolean.FALSE);
                        break;
                    case START_BILLING:
                        blockedBillingMap.put(cur.getEntitlementId(), Boolean.FALSE);
                        break;
                    case PAUSE_ENTITLEMENT:
                    case STOP_ENTITLEMENT:
                        blockedEntitlementMap.put(cur.getEntitlementId(), Boolean.TRUE);
                        break;
                    case PAUSE_BILLING:
                    case STOP_BILLING:
                        blockedBillingMap.put(cur.getEntitlementId(), Boolean.TRUE);
                        break;
                }
            } else {
                break;
            }
        }


        final DefaultSubscriptionEvent next = it.hasNext() ? (DefaultSubscriptionEvent) it.next() : null;

        final List<UUID> targetEntitlementIds = bs.getType() == BlockingStateType.SUBSCRIPTION ? ImmutableList.<UUID>of(bs.getBlockedId()) :
                                                ImmutableList.<UUID>copyOf(allEntitlementUUIDs);
        for (UUID target : targetEntitlementIds) {

            // If the blocking state is ENT_STATE_CANCELLED there is nothing else to look at, just insert the event
            if (bs.getStateName().equals(DefaultEntitlementApi.ENT_STATE_CANCELLED)) {
                newEvents.add(toSubscriptionEvent(cur, next, target, bs, SubscriptionEventType.STOP_ENTITLEMENT, accountTimeZone));
                continue;
            }

            // If not, figure out from the existing state, what that new event should be
            final Boolean isResumeEntitlement = (blockedEntitlementMap.get(target) && !bs.isBlockEntitlement());
            final Boolean isPauseEntitlement = (!blockedEntitlementMap.get(target) && bs.isBlockEntitlement());
            final Boolean isResumeBilling = (blockedBillingMap.get(target) && !bs.isBlockBilling());
            final Boolean isPauseBilling = (!blockedBillingMap.get(target) && bs.isBlockBilling());
            final Boolean isServiceStateChange = !(isResumeEntitlement || isPauseEntitlement || isResumeBilling || isPauseBilling);

            if (isResumeEntitlement) {
                newEvents.add(toSubscriptionEvent(cur, next, target, bs, SubscriptionEventType.RESUME_ENTITLEMENT, accountTimeZone));
            } else if (isPauseEntitlement) {
                newEvents.add(toSubscriptionEvent(cur, next, target, bs, SubscriptionEventType.PAUSE_ENTITLEMENT, accountTimeZone));
            }
            if (isResumeBilling) {
                newEvents.add(toSubscriptionEvent(cur, next, target, bs, SubscriptionEventType.RESUME_BILLING, accountTimeZone));
            } else if (isPauseBilling) {
                newEvents.add(toSubscriptionEvent(cur, next, target, bs, SubscriptionEventType.PAUSE_BILLING, accountTimeZone));
            }
            if (isServiceStateChange) {
                newEvents.add(toSubscriptionEvent(cur, next, target, bs, SubscriptionEventType.SERVICE_STATE_CHANGE, accountTimeZone));
            }
        }
        return index;
    }


    private LinkedList<SubscriptionEvent> computeSubscriptionBaseEvents(final List<Entitlement> entitlements, final DateTimeZone accountTimeZone) {

        final LinkedList<SubscriptionEvent> result = new LinkedList<SubscriptionEvent>();
        for (Entitlement cur : entitlements) {
            final SubscriptionBase base = ((DefaultEntitlement) cur).getSubscriptionBase();
            final List<SubscriptionBaseTransition> baseTransitions = base.getAllTransitions();
            for (SubscriptionBaseTransition tr : baseTransitions) {
                final SubscriptionEventType eventType = toEventType(tr.getTransitionType());
                if (eventType == null) {
                    continue;
                }
                final SubscriptionEvent event = toSubscriptionEvent(tr, eventType, accountTimeZone);
                insertSubscriptionEvent(event, result);
                if (tr.getTransitionType() == SubscriptionBaseTransitionType.CREATE ||
                    tr.getTransitionType() == SubscriptionBaseTransitionType.TRANSFER) {
                    final SubscriptionEvent billingEvent = toSubscriptionEvent(tr, SubscriptionEventType.START_BILLING, accountTimeZone);
                    insertSubscriptionEvent(billingEvent, result);
                }
            }
        }
        sanitizeForBaseRecreateEvents(result);
        return result;
    }


    //
    // Old version of code would use CANCEL/ RE_CREATE to simulate PAUSE_BILLING/RESUME_BILLING
    //
    private void sanitizeForBaseRecreateEvents(final LinkedList<SubscriptionEvent> input) {

        final Set<UUID> guiltyEntitlementIds = new TreeSet<UUID>();
        ListIterator<SubscriptionEvent> it = input.listIterator(input.size() - 1);
        while (it.hasPrevious()) {
            final SubscriptionEvent cur = it.previous();
            if (cur.getSubscriptionEventType() == SubscriptionEventType.RESUME_BILLING) {
                guiltyEntitlementIds.add(cur.getId());
                continue;
            }
            if (cur.getSubscriptionEventType() == SubscriptionEventType.STOP_BILLING &&
                guiltyEntitlementIds.contains(cur.getId())) {
                guiltyEntitlementIds.remove(cur.getId());
                final SubscriptionEvent correctedEvent = new DefaultSubscriptionEvent((DefaultSubscriptionEvent) cur, SubscriptionEventType.PAUSE_BILLING);
                it.set(correctedEvent);
            }
        }
    }


    private void insertSubscriptionEvent(final SubscriptionEvent event, final LinkedList<SubscriptionEvent> result) {
        int index = 0;
        for (SubscriptionEvent cur : result) {
            int compEffectiveDate = event.getEffectiveDate().compareTo(cur.getEffectiveDate());
            if (compEffectiveDate < 0) {
                // EffectiveDate is less than cur -> insert here
                break;
            } else if (compEffectiveDate == 0) {

                int compCreatedDate = ((DefaultSubscriptionEvent) event).getCreatedDate().compareTo(((DefaultSubscriptionEvent) cur).getCreatedDate());
                if (compCreatedDate < 0) {
                    // Same EffectiveDate but CreatedDate is less than cur -> insert here
                    break;
                } else if (compCreatedDate == 0) {
                    int compUUID = event.getId().compareTo(cur.getId());
                    if (compUUID < 0) {
                        // Same EffectiveDate and CreatedDate but order by ID
                        break;
                    } else if (compUUID == 0) {
                        if (event.getSubscriptionEventType().ordinal() < cur.getSubscriptionEventType().ordinal()) {
                            // Same EffectiveDate, CreatedDate and ID, but event type is lower -- as described in enum
                            break;
                        }
                    }
                }
            }
            index++;
        }
        result.add(index, event);
    }

    private SubscriptionEvent toSubscriptionEvent(final SubscriptionEvent prev, final SubscriptionEvent next, final UUID entitlementId, final BlockingState in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
        return new DefaultSubscriptionEvent(in.getId(),
                                            entitlementId,
                                            new LocalDate(in.getEffectiveDate(), accountTimeZone),
                                            new LocalDate(in.getCreatedDate(), accountTimeZone),
                                            eventType,
                                            in.isBlockEntitlement(),
                                            in.isBlockBilling(),
                                            in.getService(),
                                            in.getStateName(),
                                            prev != null ? prev.getNextProduct() : null,
                                            prev != null ? prev.getNextPlan() : null,
                                            prev != null ? prev.getNextPhase() : null,
                                            prev != null ? prev.getNextPriceList() : null,
                                            prev != null ? prev.getNextBillingPeriod() : null,
                                            next != null ? next.getPrevProduct() : null,
                                            next != null ? next.getPrevPlan() : null,
                                            next != null ? next.getPrevPhase() : null,
                                            next != null ? next.getPrevPriceList() : null,
                                            next != null ? next.getPrevBillingPeriod() : null,
                                            in.getCreatedDate());
    }

    private SubscriptionEvent toSubscriptionEvent(final SubscriptionBaseTransition in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
        return new DefaultSubscriptionEvent(in.getId(),
                                            in.getSubscriptionId(),
                                            new LocalDate(in.getEffectiveTransitionTime(), accountTimeZone),
                                            new LocalDate(in.getRequestedTransitionTime(), accountTimeZone),
                                            eventType,
                                            false,
                                            false,
                                            DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
                                            eventType.toString(),
                                            (in.getPreviousPlan() != null ? in.getPreviousPlan().getProduct() : null),
                                            in.getPreviousPlan(),
                                            in.getPreviousPhase(),
                                            in.getPreviousPriceList(),
                                            (in.getPreviousPlan() != null ? in.getPreviousPlan().getBillingPeriod() : null),
                                            (in.getNextPlan() != null ? in.getNextPlan().getProduct() : null),
                                            in.getNextPlan(),
                                            in.getNextPhase(),
                                            in.getNextPriceList(),
                                            (in.getNextPlan() != null ? in.getNextPlan().getBillingPeriod() : null),
                                            in.getCreatedDate());
    }

    private SubscriptionEventType toEventType(final SubscriptionBaseTransitionType in) {
        switch (in) {
            case CREATE:
                return SubscriptionEventType.START_ENTITLEMENT;
            case MIGRATE_ENTITLEMENT:
                return SubscriptionEventType.START_ENTITLEMENT;
            case TRANSFER:
                return SubscriptionEventType.START_ENTITLEMENT;
            case MIGRATE_BILLING:
                return SubscriptionEventType.START_BILLING;
            case CHANGE:
                return SubscriptionEventType.CHANGE;
            case CANCEL:
                return SubscriptionEventType.STOP_BILLING;
            case PHASE:
                return SubscriptionEventType.PHASE;
            // STEPH This is the old way of pausing billing; not used any longer, but kept for compatibility reason
            case RE_CREATE:
                return SubscriptionEventType.RESUME_BILLING;
            /*
             * Those can be ignore:
             */
            // 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 null;
        }
    }


    @Override
    public UUID getAccountId() {
        return accountId;
    }

    @Override
    public UUID getBundleId() {
        return bundleId;
    }

    @Override
    public String getExternalKey() {
        return externalKey;
    }

    @Override
    public List<SubscriptionEvent> getSubscriptionEvents() {
        return events;
    }

    private static final class DefaultSubscriptionEvent implements SubscriptionEvent {

        private final UUID id;
        private final UUID entitlementId;
        private final LocalDate effectiveDate;
        private final LocalDate requestedDate;
        private final SubscriptionEventType eventType;
        private final boolean isBlockingEntitlement;
        private final boolean isBlockingBilling;
        private final String serviceName;
        private final String serviceStateName;
        private final Product prevProduct;
        private final Plan prevPlan;
        private final PlanPhase prevPlanPhase;
        private final PriceList prevPriceList;
        private final BillingPeriod prevBillingPeriod;
        private final Product nextProduct;
        private final Plan nextPlan;
        private final PlanPhase nextPlanPhase;
        private final PriceList nextPriceList;
        private final BillingPeriod nextBillingPeriod;
        private final DateTime createdDate;


        private DefaultSubscriptionEvent(final UUID id,
                                         final UUID entitlementId,
                                         final LocalDate effectiveDate,
                                         final LocalDate requestedDate,
                                         final SubscriptionEventType eventType,
                                         final boolean blockingEntitlement,
                                         final boolean blockingBilling,
                                         final String serviceName,
                                         final String serviceStateName,
                                         final Product prevProduct,
                                         final Plan prevPlan,
                                         final PlanPhase prevPlanPhase,
                                         final PriceList prevPriceList,
                                         final BillingPeriod prevBillingPeriod,
                                         final Product nextProduct,
                                         final Plan nextPlan,
                                         final PlanPhase nextPlanPhase,
                                         final PriceList nextPriceList,
                                         final BillingPeriod nextBillingPeriod,
                                         final DateTime createDate) {
            this.id = id;
            this.entitlementId = entitlementId;
            this.effectiveDate = effectiveDate;
            this.requestedDate = requestedDate;
            this.eventType = eventType;
            isBlockingEntitlement = blockingEntitlement;
            isBlockingBilling = blockingBilling;
            this.serviceName = serviceName;
            this.serviceStateName = serviceStateName;
            this.prevProduct = prevProduct;
            this.prevPlan = prevPlan;
            this.prevPlanPhase = prevPlanPhase;
            this.prevPriceList = prevPriceList;
            this.prevBillingPeriod = prevBillingPeriod;
            this.nextProduct = nextProduct;
            this.nextPlan = nextPlan;
            this.nextPlanPhase = nextPlanPhase;
            this.nextPriceList = nextPriceList;
            this.nextBillingPeriod = nextBillingPeriod;
            this.createdDate = createDate;
        }

        private DefaultSubscriptionEvent(DefaultSubscriptionEvent copy, SubscriptionEventType newEventType) {
            this(copy.getId(),
                 copy.getEntitlementId(),
                 copy.getEffectiveDate(),
                 copy.getRequestedDate(),
                 newEventType,
                 copy.isBlockedEntitlement(),
                 copy.isBlockedBilling(),
                 copy.getServiceName(),
                 copy.getServiceStateName(),
                 copy.getPrevProduct(),
                 copy.getPrevPlan(),
                 copy.getPrevPhase(),
                 copy.getPrevPriceList(),
                 copy.getPrevBillingPeriod(),
                 copy.getNextProduct(),
                 copy.getNextPlan(),
                 copy.getNextPhase(),
                 copy.getNextPriceList(),
                 copy.getNextBillingPeriod(),
                 copy.getCreatedDate());
        }


        @Override
        public UUID getId() {
            return id;
        }

        @Override
        public UUID getEntitlementId() {
            return entitlementId;
        }

        @Override
        public LocalDate getEffectiveDate() {
            return effectiveDate;
        }

        @Override
        public LocalDate getRequestedDate() {
            return requestedDate;
        }

        @Override
        public SubscriptionEventType getSubscriptionEventType() {
            return eventType;
        }

        @Override
        public boolean isBlockedBilling() {
            return isBlockingBilling;
        }

        @Override
        public boolean isBlockedEntitlement() {
            return isBlockingEntitlement;
        }

        @Override
        public String getServiceName() {
            return serviceName;
        }

        @Override
        public String getServiceStateName() {
            return serviceStateName;
        }

        @Override
        public Product getPrevProduct() {
            return prevProduct;
        }

        @Override
        public Plan getPrevPlan() {
            return prevPlan;
        }

        @Override
        public PlanPhase getPrevPhase() {
            return prevPlanPhase;
        }

        @Override
        public PriceList getPrevPriceList() {
            return prevPriceList;
        }

        @Override
        public BillingPeriod getPrevBillingPeriod() {
            return prevBillingPeriod;
        }

        @Override
        public Product getNextProduct() {
            return nextProduct;
        }

        @Override
        public Plan getNextPlan() {
            return nextPlan;
        }

        @Override
        public PlanPhase getNextPhase() {
            return nextPlanPhase;
        }

        @Override
        public PriceList getNextPriceList() {
            return nextPriceList;
        }

        @Override
        public BillingPeriod getNextBillingPeriod() {
            return nextBillingPeriod;
        }

        public DateTime getCreatedDate() {
            return createdDate;
        }
    }
}