PlanAligner.java

306 lines | 13.148 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.alignment;

import com.google.inject.Inject;
import com.ning.billing.ErrorCode;
import com.ning.billing.catalog.api.*;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.SubscriptionData;
import com.ning.billing.entitlement.api.user.SubscriptionTransitionData;
import com.ning.billing.entitlement.exceptions.EntitlementError;
import com.ning.billing.util.clock.DefaultClock;
import org.joda.time.DateTime;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 *
 * PlanAligner offers specific APIs to return the correct {@code TimedPhase} when creating, changing Plan or to compute next Phase on current Plan.
 * <p>
 *
 */
public class PlanAligner  {

    private final CatalogService catalogService;

    @Inject
    public PlanAligner(CatalogService catalogService) {
        this.catalogService = catalogService;
    }

    private enum WhichPhase {
        CURRENT,
        NEXT
    }

    /**
     * Returns the current and next phase for the subscription in creation
     * <p>
     * @param subscription the subscription in creation
     * @param plan the current Plan
     * @param initialPhase the initialPhase on which we should create that subscription. can be null
     * @param priceList the priceList
     * @param effectiveDate the effective creation date
     * @return
     * @throws CatalogApiException
     * @throws EntitlementUserApiException
     */
    public TimedPhase [] getCurrentAndNextTimedPhaseOnCreate(final SubscriptionData subscription,
            final Plan plan, final PhaseType initialPhase, final String priceList, final DateTime requestedDate, final DateTime effectiveDate)
        throws CatalogApiException, EntitlementUserApiException {
        List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription.getStartDate(),
                subscription.getBundleStartDate(), plan, initialPhase, priceList, requestedDate);
        TimedPhase [] result = new TimedPhase[2];
        result[0] = getTimedPhase(timedPhases, effectiveDate, WhichPhase.CURRENT);
        result[1] = getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
        return result;
    }

    /**
     *
     * Returns current Phase for that Plan change
     * <p>
     * @param subscription the subscription in creation
     * @param plan the current Plan
     * @param priceList the priceList on which we should change that subscription.
     * @param effectiveDate the effective change date
     * @return
     * @throws CatalogApiException
     * @throws EntitlementUserApiException
     */
    public TimedPhase getCurrentTimedPhaseOnChange(final SubscriptionData subscription,
            final Plan plan, final String priceList, final DateTime requestedDate, final DateTime effectiveDate)
        throws CatalogApiException, EntitlementUserApiException {
        return getTimedPhaseOnChange(subscription, plan, priceList, requestedDate, effectiveDate, WhichPhase.CURRENT);
    }

    /**
     * Returns next Phase for that Plan change
     * <p>
     * @param subscription the subscription in creation
     * @param plan the current Plan
     * @param priceList the priceList on which we should change that subscription.
     * @param effectiveDate the effective change date
     * @return
     * @throws CatalogApiException
     * @throws EntitlementUserApiException
     */
    public TimedPhase getNextTimedPhaseOnChange(final SubscriptionData subscription,
            final Plan plan, final String priceList, final DateTime requestedDate, final DateTime effectiveDate)
        throws CatalogApiException, EntitlementUserApiException {
        return getTimedPhaseOnChange(subscription, plan, priceList, requestedDate, effectiveDate, WhichPhase.NEXT);
    }


    /**
     * Returns next Phase for that Subscription at a point in time
     * <p>
     * @param subscription the subscription for which we need to compute the next Phase event
     * @param effectiveDate the date at which we look to compute that event. effective needs to be after last Plan change or initial Plan
     * @return The PhaseEvent at the correct point in time
     */
    public TimedPhase getNextTimedPhase(final SubscriptionData subscription, final DateTime requestedDate, final DateTime effectiveDate) {
        try {

            SubscriptionTransitionData lastPlanTransition = subscription.getInitialTransitionForCurrentPlan();
            if (effectiveDate.isBefore(lastPlanTransition.getEffectiveTransitionTime())) {
                throw new EntitlementError(String.format("Cannot specify an effectiveDate prior to last Plan Change, subscription = %s, effectiveDate = %s",
                        subscription.getId(), effectiveDate));
            }

            switch(lastPlanTransition.getTransitionType()) {
            // If we never had any Plan change, borrow the logics for createPlan alignment
            case MIGRATE_ENTITLEMENT:
            case CREATE:
            case RE_CREATE:                
                List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription.getStartDate(),
                        subscription.getBundleStartDate(),
                        lastPlanTransition.getNextPlan(),
                        lastPlanTransition.getNextPhase().getPhaseType(),
                        lastPlanTransition.getNextPriceList().getName(),
                        requestedDate);
                return getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
            // If we went through Plan changes, borrow the logics for changePlan alignement
            case CHANGE:
                return getTimedPhaseOnChange(subscription.getStartDate(),
                        subscription.getBundleStartDate(),
                        lastPlanTransition.getPreviousPhase(),
                        lastPlanTransition.getPreviousPlan(),
                        lastPlanTransition.getPreviousPriceList().getName(),
                        lastPlanTransition.getNextPlan(),
                        lastPlanTransition.getNextPriceList().getName(),
                        requestedDate,
                        effectiveDate,
                        WhichPhase.NEXT);
            default:
                throw new EntitlementError(String.format("Unexpectd initial transition %s for current plan %s on subscription %s",
                        lastPlanTransition.getTransitionType(), subscription.getCurrentPlan(), subscription.getId()));
            }
        } catch (Exception /* EntitlementUserApiException, CatalogApiException */ e) {
            throw new EntitlementError(String.format("Could not compute next phase change for subscription %s", subscription.getId()), e);
        }
    }


    private List<TimedPhase> getTimedPhaseOnCreate(DateTime subscriptionStartDate,
            DateTime bundleStartDate,
            Plan plan, PhaseType initialPhase, String priceList, DateTime requestedDate)
        throws CatalogApiException, EntitlementUserApiException  {

        Catalog catalog = catalogService.getFullCatalog();

        PlanSpecifier planSpecifier = new PlanSpecifier(plan.getProduct().getName(),
                plan.getProduct().getCategory(),
                plan.getBillingPeriod(),
                priceList);

        DateTime planStartDate = null;
        PlanAlignmentCreate alignement = catalog.planCreateAlignment(planSpecifier, requestedDate);
        switch(alignement) {
        case START_OF_SUBSCRIPTION:
            planStartDate = subscriptionStartDate;
            break;
        case START_OF_BUNDLE:
            planStartDate = bundleStartDate;
            break;
        default:
            throw new EntitlementError(String.format("Unknwon PlanAlignmentCreate %s", alignement));
        }
        return getPhaseAlignments(plan, initialPhase, planStartDate);
    }

    private TimedPhase getTimedPhaseOnChange(SubscriptionData subscription,
            Plan nextPlan, String nextPriceList, DateTime requestedDate, DateTime effectiveDate, WhichPhase which)
        throws CatalogApiException, EntitlementUserApiException {
        return getTimedPhaseOnChange(subscription.getStartDate(),
                subscription.getBundleStartDate(),
                subscription.getCurrentPhase(),
                subscription.getCurrentPlan(),
                subscription.getCurrentPriceList().getName(),
                nextPlan,
                nextPriceList,
                requestedDate,
                effectiveDate,
                which);
    }


    private TimedPhase getTimedPhaseOnChange(DateTime subscriptionStartDate,
            DateTime bundleStartDate,
            PlanPhase currentPhase,
            Plan currentPlan,
            String currentPriceList,
            Plan nextPlan, String priceList, DateTime requestedDate, DateTime effectiveDate, WhichPhase which)
        throws CatalogApiException, EntitlementUserApiException {

        Catalog catalog = catalogService.getFullCatalog();
        ProductCategory currentCategory = currentPlan.getProduct().getCategory();
        PlanPhaseSpecifier fromPlanPhaseSpecifier = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
                currentCategory,
                currentPlan.getBillingPeriod(),
                currentPriceList,
                currentPhase.getPhaseType());

        PlanSpecifier toPlanSpecifier = new PlanSpecifier(nextPlan.getProduct().getName(),
                nextPlan.getProduct().getCategory(),
                nextPlan.getBillingPeriod(),
                priceList);

        DateTime planStartDate = null;
        PlanAlignmentChange alignment = catalog.planChangeAlignment(fromPlanPhaseSpecifier, toPlanSpecifier, requestedDate);
        switch(alignment) {
        case START_OF_SUBSCRIPTION:
            planStartDate = subscriptionStartDate;
            break;
        case START_OF_BUNDLE:
            planStartDate = bundleStartDate;
            break;
        case CHANGE_OF_PLAN:
            planStartDate = requestedDate;
            break;
        case CHANGE_OF_PRICELIST:
            throw new EntitlementError(String.format("Not implemented yet %s", alignment));
        default:
            throw new EntitlementError(String.format("Unknwon PlanAlignmentChange %s", alignment));
        }
        List<TimedPhase> timedPhases = getPhaseAlignments(nextPlan, null, planStartDate);
        return getTimedPhase(timedPhases, effectiveDate, which);
    }


    private List<TimedPhase> getPhaseAlignments(Plan plan, PhaseType initialPhase, DateTime initialPhaseStartDate)
        throws EntitlementUserApiException {
        if (plan == null) {
            return Collections.emptyList();
        }

        List<TimedPhase> result = new LinkedList<TimedPhase>();
        DateTime curPhaseStart = (initialPhase == null) ? initialPhaseStartDate : null;
        DateTime nextPhaseStart = null;
        for (PlanPhase cur : plan.getAllPhases()) {
            // For create we can specify the phase so skip any phase until we reach initialPhase
            if (curPhaseStart == null) {
                if (initialPhase != cur.getPhaseType()) {
                    continue;
                }
                curPhaseStart = initialPhaseStartDate;
            }

            result.add(new TimedPhase(cur, curPhaseStart));

            // STEPH check for duration null instead TimeUnit UNLIMITED
            if (cur.getPhaseType() != PhaseType.EVERGREEN) {
                Duration curPhaseDuration = cur.getDuration();
                nextPhaseStart = DefaultClock.addDuration(curPhaseStart, curPhaseDuration);
                if (nextPhaseStart == null) {
                    throw new EntitlementError(String.format("Unexpected non ending UNLIMITED phase for plan %s",
                            plan.getName()));
                }
                curPhaseStart = nextPhaseStart;
            }
        }
        if (initialPhase != null && curPhaseStart == null) {
            throw new EntitlementUserApiException(ErrorCode.ENT_CREATE_BAD_PHASE, initialPhase);
        }
        return result;
    }

    // STEPH check for non evergreen Plans and what happens
    private TimedPhase getTimedPhase(List<TimedPhase> timedPhases, DateTime effectiveDate, WhichPhase which) {
        TimedPhase cur = null;
        TimedPhase next = null;
        for (TimedPhase phase : timedPhases) {
            if (phase.getStartPhase().isAfter(effectiveDate)) {
                next = phase;
                break;
            }
            cur = phase;
        }
        switch(which) {
        case CURRENT:
            return cur;
        case NEXT:
            return next;
        default:
            throw new EntitlementError(String.format("Unexpected %s TimedPhase", which));
        }
    }
}