DefaultEntitlementRepairApi.java

342 lines | 15.541 kB Blame History Raw Download
/* 
 * Copyright 2010-2011 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.repair;

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.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;

import org.joda.time.DateTime;

import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.ning.billing.ErrorCode;
import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.entitlement.api.SubscriptionFactory;
import com.ning.billing.entitlement.api.repair.SubscriptionRepair.ExistingEvent;
import com.ning.billing.entitlement.api.repair.SubscriptionRepair.NewEvent;
import com.ning.billing.entitlement.api.user.Subscription;
import com.ning.billing.entitlement.api.user.SubscriptionBundle;
import com.ning.billing.entitlement.api.user.SubscriptionBundleData;
import com.ning.billing.entitlement.api.user.SubscriptionData;
import com.ning.billing.entitlement.api.user.DefaultSubscriptionFactory.SubscriptionBuilder;
import com.ning.billing.entitlement.api.user.SubscriptionTransitionData;
import com.ning.billing.entitlement.engine.dao.EntitlementDao;
import com.ning.billing.entitlement.events.EntitlementEvent;
import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
import com.ning.billing.entitlement.exceptions.EntitlementError;
import com.ning.billing.entitlement.glue.EntitlementModule;
import com.ning.billing.util.callcontext.CallContext;

public class DefaultEntitlementRepairApi implements EntitlementRepairApi {

    private final EntitlementDao dao;
    private final SubscriptionFactory factory;
    private final RepairEntitlementLifecycleDao repairDao;

    @Inject
    public DefaultEntitlementRepairApi(@Named(EntitlementModule.REPAIR_NAMED) final SubscriptionFactory factory,
            @Named(EntitlementModule.REPAIR_NAMED) final RepairEntitlementLifecycleDao repairDao, final EntitlementDao dao) {
        this.dao = dao;
        this.repairDao = repairDao;
        this.factory = factory;
    }
    

    @Override
    public BundleRepair getBundleRepair(final UUID bundleId) 
    throws EntitlementRepairException {

        SubscriptionBundle bundle = dao.getSubscriptionBundleFromId(bundleId);
        if (bundle == null) {
            throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_UNKNOWN_BUNDLE, bundleId);
        }
        final List<Subscription> subscriptions = dao.getSubscriptions(factory, bundleId);
        if (subscriptions.size() == 0) {
            throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_NO_ACTIVE_SUBSCRIPTIONS, bundleId);
        }
        final String viewId = getViewId(((SubscriptionBundleData) bundle).getLastSysUpdateTime(), subscriptions);
        final List<SubscriptionRepair> repairs = createGetSubscriptionRepairList(subscriptions, Collections.<SubscriptionRepair>emptyList()); 
        return createGetBundleRepair(bundleId, viewId, repairs);
    }



    @Override
    public BundleRepair repairBundle(final BundleRepair input, final boolean dryRun, final CallContext context)
    throws EntitlementRepairException {

        try {
            
            SubscriptionBundle bundle = dao.getSubscriptionBundleFromId(input.getBundleId());
            if (bundle == null) {
                throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_UNKNOWN_BUNDLE, input.getBundleId());
            }
            
            // Subscriptions are ordered with BASE subscription first-- if exists
            final List<Subscription> subscriptions = dao.getSubscriptions(factory, input.getBundleId());
            if (subscriptions.size() == 0) {
                throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_UNKNOWN_BUNDLE,input.getBundleId());
            }
            
            final String viewId = getViewId(((SubscriptionBundleData) bundle).getLastSysUpdateTime(), subscriptions);
            if (!viewId.equals(input.getViewId())) {
                throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_VIEW_CHANGED,input.getBundleId(), input.getViewId(), viewId);
            }
            
            DateTime firstDeletedBPEventTime = null;
            DateTime lastRemainingBPEventTime = null;

            SubscriptionDataRepair baseSubscriptionRepair = null;
            List<SubscriptionDataRepair> addOnSubscriptionInRepair = new LinkedList<SubscriptionDataRepair>();
            List<SubscriptionDataRepair> inRepair =  new LinkedList<SubscriptionDataRepair>();
            for (Subscription cur : subscriptions) {

                SubscriptionRepair curRepair = findAndCreateSubscriptionRepair(cur.getId(), input.getSubscriptions());
                if (curRepair != null) {
                    SubscriptionDataRepair curData = ((SubscriptionDataRepair) cur);
                    List<EntitlementEvent> remaining = getRemainingEventsAndValidateDeletedEvents(curData, firstDeletedBPEventTime, curRepair.getDeletedEvents());

                    if (cur.getCategory() == ProductCategory.BASE) {
                        int bpTransitionSize =((SubscriptionData) cur).getAllTransitions().size();
                        lastRemainingBPEventTime = (remaining.size() > 0) ? curData.getAllTransitions().get(remaining.size() - 1).getEffectiveTransitionTime() : null;
                        firstDeletedBPEventTime =  (remaining.size() < bpTransitionSize) ? curData.getAllTransitions().get(remaining.size()).getEffectiveTransitionTime() : null;
                    }

                    if (curRepair.getNewEvents() != null && curRepair.getNewEvents().size() > 0) {
                        Collections.sort(curRepair.getNewEvents(), new Comparator<NewEvent>() {
                            @Override
                            public int compare(NewEvent o1, NewEvent o2) {
                                return o1.getRequestedDate().compareTo(o2.getRequestedDate());
                            }
                        });
                        DateTime lastRemainingEventTime = (remaining.size() == 0) ? null : curData.getAllTransitions().get(remaining.size() - 1).getEffectiveTransitionTime();
                        validateFirstNewEvent(curData, curRepair.getNewEvents().get(0), lastRemainingBPEventTime, lastRemainingEventTime);
                    }
                    SubscriptionDataRepair sRepair = createSubscriptionDataRepair(curData, remaining);
                    repairDao.initializeRepair(curData.getId(), remaining);
                    inRepair.add(sRepair);
                    if (sRepair.getCategory() == ProductCategory.ADD_ON) {
                        addOnSubscriptionInRepair.add(sRepair);
                    } else if (sRepair.getCategory() == ProductCategory.BASE) {
                        baseSubscriptionRepair = sRepair;
                    }
                }
            }

            if (input.getSubscriptions().size() != inRepair.size()) {
                for (SubscriptionRepair cur : input.getSubscriptions()) {
                    boolean found = false;
                    for (Subscription s : subscriptions) {
                        if (s.getId().equals(cur.getId())) {
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_UNKNOWN_SUBSCRIPTION, cur.getId());
                    }
                }
            }

            TreeSet<NewEvent> newEventSet = new TreeSet<SubscriptionRepair.NewEvent>(new Comparator<NewEvent>() {
                @Override
                public int compare(NewEvent o1, NewEvent o2) {
                    return o1.getRequestedDate().compareTo(o2.getRequestedDate());
                }
            });
            for (SubscriptionRepair cur : input.getSubscriptions()) {
                for (NewEvent e : cur.getNewEvents()) {
                    newEventSet.add(new DefaultNewEvent(cur.getId(), e.getPlanPhaseSpecifier(), e.getRequestedDate(), e.getSubscriptionTransitionType()));    
                }
            }

            Iterator<NewEvent> it = newEventSet.iterator();
            while (it.hasNext()) {
                DefaultNewEvent cur = (DefaultNewEvent) it.next();
                SubscriptionDataRepair curDataRepair = findSubscriptionDataRepair(cur.getSubscriptionId(), inRepair);
                if (curDataRepair == null) {
                    throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_UNKNOWN_SUBSCRIPTION, cur.getSubscriptionId());
                }
                curDataRepair.addNewRepairEvent(cur, baseSubscriptionRepair, addOnSubscriptionInRepair, context);
            }

            if (dryRun) {
                final List<SubscriptionRepair> repairs = createGetSubscriptionRepairList(subscriptions, convertDataRepair(inRepair)); 
                return createGetBundleRepair(input.getBundleId(), input.getViewId(), repairs);
            } else {
                // STEPH no implemented yet
                return null;
            }
        } finally {
            repairDao.cleanup();
        }
    }
    
    private String getViewId(DateTime lastUpdateBundleDate, List<Subscription> subscriptions) {
        StringBuilder tmp = new StringBuilder();
        long lastOrderedId = -1;
        for (Subscription cur : subscriptions) {
            lastOrderedId = lastOrderedId < ((SubscriptionData) cur).getLastEventOrderedId() ? ((SubscriptionData) cur).getLastEventOrderedId() : lastOrderedId;
        }
        tmp.append(lastOrderedId);
        tmp.append("-");
        tmp.append(lastUpdateBundleDate.toDate().getTime());
        return tmp.toString();
    }

    private BundleRepair createGetBundleRepair(final UUID bundleId, final String viewId, final List<SubscriptionRepair> repairList) {
        return new BundleRepair() {
            @Override
            public String getViewId() {
                return viewId;
            }
            @Override
            public List<SubscriptionRepair> getSubscriptions() {
                return repairList;
            }
            @Override
            public UUID getBundleId() {
                return bundleId;
            }
        };

    }
    
    private List<SubscriptionRepair> createGetSubscriptionRepairList(final List<Subscription> subscriptions, final List<SubscriptionRepair> inRepair) {

        final List<SubscriptionRepair> result = new LinkedList<SubscriptionRepair>();
        Set<UUID> repairIds = new TreeSet<UUID>();
        for (final SubscriptionRepair cur : inRepair) {
            repairIds.add(cur.getId());
            result.add(cur);
        }
        for (final Subscription cur : subscriptions) {
            if ( !repairIds.contains(cur.getId())) { 
                result.add(new DefaultSubscriptionRepair((SubscriptionData) cur));
            }
        }
        return result;
    }


    private List<SubscriptionRepair> convertDataRepair(List<SubscriptionDataRepair> input) {
        List<SubscriptionRepair> result = new LinkedList<SubscriptionRepair>();
        for (SubscriptionDataRepair cur : input) {
            result.add(new DefaultSubscriptionRepair(cur));
        }
        return result;
    }

    private SubscriptionDataRepair findSubscriptionDataRepair(final UUID targetId, final List<SubscriptionDataRepair> input) {
        for (SubscriptionDataRepair cur : input) {
            if (cur.getId().equals(targetId)) {
                return cur;
            }
        }
        return null;
    }


    private SubscriptionDataRepair createSubscriptionDataRepair(final SubscriptionData curData, final List<EntitlementEvent> initialEvents) {
        SubscriptionBuilder builder = new SubscriptionBuilder(curData);
        builder.setActiveVersion(curData.getActiveVersion() + 1);
        SubscriptionDataRepair result = (SubscriptionDataRepair) factory.createSubscription(builder, initialEvents);
        return result;
    }

    private void validateFirstNewEvent(final SubscriptionData data, final NewEvent firstNewEvent, final DateTime lastBPRemainingTime, final DateTime lastRemainingTime) 
    throws EntitlementRepairException {
        if (lastBPRemainingTime != null &&
                firstNewEvent.getRequestedDate().isBefore(lastBPRemainingTime)) {
            throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_NEW_AO_EVENT_BEFORE_BP, firstNewEvent.getPlanPhaseSpecifier().toString(), data.getId());
        }
        if (lastRemainingTime != null &&
                firstNewEvent.getRequestedDate().isBefore(lastRemainingTime)) {
            throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_INVALID_NEW_AO_EVENT, firstNewEvent.getPlanPhaseSpecifier().toString(), data.getId());
        }

    }

    private List<EntitlementEvent> getRemainingEventsAndValidateDeletedEvents(final SubscriptionDataRepair data, final DateTime firstBPDeletedTime,
            final List<SubscriptionRepair.DeletedEvent> deletedEvents) 
            throws EntitlementRepairException  {

        if (deletedEvents == null || deletedEvents.size() == 0) {
            return data.getEvents();
        }

        int nbDeleted = 0;
        LinkedList<EntitlementEvent> result = new LinkedList<EntitlementEvent>();
        for (EntitlementEvent cur : data.getEvents()) {

            boolean foundDeletedEvent = false;
            for (SubscriptionRepair.DeletedEvent d : deletedEvents) {
                if (cur.getId().equals(d.getEventId())) {
                    foundDeletedEvent = true;
                    nbDeleted++;
                    break;
                }
            }
            if (!foundDeletedEvent && nbDeleted > 0) {
                throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_INVALID_DELETE_SET, cur.getId(), data.getId());
            }
            if (firstBPDeletedTime != null && 
                    ! cur.getEffectiveDate().isBefore(firstBPDeletedTime) &&
                    ! foundDeletedEvent) {
                throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_MISSING_AO_DELETE_EVENT, cur.getId(), data.getId());                
            }

            if (nbDeleted == 0) {
                result.add(cur);
            }
        }
        if (nbDeleted != deletedEvents.size()) {
            for (SubscriptionRepair.DeletedEvent d : deletedEvents) {
                boolean found = false;
                for (SubscriptionTransitionData cur : data.getAllTransitions()) {
                    if (cur.getId().equals(d.getEventId())) {
                        found = true;
                        continue;
                    }
                }
                if (!found) {
                    throw new EntitlementRepairException(ErrorCode.ENT_REPAIR_NON_EXISTENT_DELETE_EVENT, d.getEventId(), data.getId());
                }
            }

        }
        return result;
    }

    private SubscriptionRepair findAndCreateSubscriptionRepair(final UUID target, final List<SubscriptionRepair> input) {
        for (SubscriptionRepair cur : input) {
            if (target.equals(cur.getId())) {
                return new DefaultSubscriptionRepair(cur);
            }
        }
        return null;
    }
}