PlanAligner.java

343 lines | 18.239 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.subscription.alignment;

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

import javax.annotation.Nullable;
import javax.inject.Inject;

import org.joda.time.DateTime;

import com.ning.billing.ErrorCode;
import com.ning.billing.catalog.api.Catalog;
import com.ning.billing.catalog.api.CatalogApiException;
import com.ning.billing.catalog.api.CatalogService;
import com.ning.billing.catalog.api.Duration;
import com.ning.billing.catalog.api.PhaseType;
import com.ning.billing.catalog.api.Plan;
import com.ning.billing.catalog.api.PlanAlignmentChange;
import com.ning.billing.catalog.api.PlanAlignmentCreate;
import com.ning.billing.catalog.api.PlanPhase;
import com.ning.billing.catalog.api.PlanPhaseSpecifier;
import com.ning.billing.catalog.api.PlanSpecifier;
import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.subscription.api.user.SubscriptionBaseApiException;
import com.ning.billing.subscription.api.user.SubscriptionBaseTransitionData;
import com.ning.billing.subscription.api.user.DefaultSubscriptionBase;
import com.ning.billing.subscription.exceptions.SubscriptionBaseError;

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

    private final CatalogService catalogService;

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

    private enum WhichPhase {
        CURRENT,
        NEXT
    }

    /**
     * Returns the current and next phase for the subscription in creation
     *
     * @param subscription  the subscription in creation (only the start date and the bundle start date are looked at)
     * @param plan          the current Plan
     * @param initialPhase  the initialPhase on which we should create that subscription. can be null
     * @param priceList     the priceList
     * @param requestedDate the requested date (only used to load the catalog)
     * @param effectiveDate the effective creation date (driven by the catalog policy, i.e. when the creation occurs)
     * @return the current and next phases
     * @throws CatalogApiException         for catalog errors
     * @throws com.ning.billing.subscription.api.user.SubscriptionBaseApiException for subscription errors
     */
    public TimedPhase[] getCurrentAndNextTimedPhaseOnCreate(final DefaultSubscriptionBase subscription,
                                                            final Plan plan,
                                                            final PhaseType initialPhase,
                                                            final String priceList,
                                                            final DateTime requestedDate,
                                                            final DateTime effectiveDate) throws CatalogApiException, SubscriptionBaseApiException {
        final List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription.getAlignStartDate(),
                                                                   subscription.getBundleStartDate(),
                                                                   plan,
                                                                   initialPhase,
                                                                   priceList,
                                                                   requestedDate);
        final 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
     *
     * @param subscription  the subscription in change (only start date, bundle start date, current phase, plan and pricelist
     *                      are looked at)
     * @param plan          the current Plan
     * @param priceList     the priceList on which we should change that subscription.
     * @param requestedDate the requested date
     * @param effectiveDate the effective change date (driven by the catalog policy, i.e. when the change occurs)
     * @return the current phase
     * @throws CatalogApiException         for catalog errors
     * @throws com.ning.billing.subscription.api.user.SubscriptionBaseApiException for subscription errors
     */
    public TimedPhase getCurrentTimedPhaseOnChange(final DefaultSubscriptionBase subscription,
                                                   final Plan plan,
                                                   final String priceList,
                                                   final DateTime requestedDate,
                                                   final DateTime effectiveDate) throws CatalogApiException, SubscriptionBaseApiException {
        return getTimedPhaseOnChange(subscription, plan, priceList, requestedDate, effectiveDate, WhichPhase.CURRENT);
    }

    /**
     * Returns next Phase for that Plan change
     *
     * @param subscription  the subscription in change (only start date, bundle start date, current phase, plan and pricelist
     *                      are looked at)
     * @param plan          the current Plan
     * @param priceList     the priceList on which we should change that subscription.
     * @param requestedDate the requested date
     * @param effectiveDate the effective change date (driven by the catalog policy, i.e. when the change occurs)
     * @return the next phase
     * @throws CatalogApiException         for catalog errors
     * @throws com.ning.billing.subscription.api.user.SubscriptionBaseApiException for subscription errors
     */
    public TimedPhase getNextTimedPhaseOnChange(final DefaultSubscriptionBase subscription,
                                                final Plan plan,
                                                final String priceList,
                                                final DateTime requestedDate,
                                                final DateTime effectiveDate) throws CatalogApiException, SubscriptionBaseApiException {
        return getTimedPhaseOnChange(subscription, plan, priceList, requestedDate, effectiveDate, WhichPhase.NEXT);
    }

    /**
     * Returns next Phase for that SubscriptionBase at a point in time
     *
     * @param subscription  the subscription for which we need to compute the next Phase event
     * @param requestedDate the requested date
     * @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 next phase
     */
    public TimedPhase getNextTimedPhase(final DefaultSubscriptionBase subscription, final DateTime requestedDate, final DateTime effectiveDate) {
        try {
            final SubscriptionBaseTransitionData lastPlanTransition = subscription.getInitialTransitionForCurrentPlan();
            if (effectiveDate.isBefore(lastPlanTransition.getEffectiveTransitionTime())) {
                throw new SubscriptionBaseError(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 logic for createPlan alignment
                case MIGRATE_ENTITLEMENT:
                case CREATE:
                case RE_CREATE:
                case TRANSFER:
                    final List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription.getAlignStartDate(),
                                                                               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 logic for changePlanWithRequestedDate alignment
                case CHANGE:
                    return getTimedPhaseOnChange(subscription.getAlignStartDate(),
                                                 subscription.getBundleStartDate(),
                                                 lastPlanTransition.getPreviousPhase(),
                                                 lastPlanTransition.getPreviousPlan(),
                                                 lastPlanTransition.getPreviousPriceList().getName(),
                                                 lastPlanTransition.getNextPlan(),
                                                 lastPlanTransition.getNextPriceList().getName(),
                                                 requestedDate,
                                                 effectiveDate,
                                                 WhichPhase.NEXT);
                default:
                    throw new SubscriptionBaseError(String.format("Unexpected initial transition %s for current plan %s on subscription %s",
                                                             lastPlanTransition.getTransitionType(), subscription.getCurrentPlan(), subscription.getId()));
            }
        } catch (Exception /* SubscriptionBaseApiException, CatalogApiException */ e) {
            throw new SubscriptionBaseError(String.format("Could not compute next phase change for subscription %s", subscription.getId()), e);
        }
    }

    private List<TimedPhase> getTimedPhaseOnCreate(final DateTime subscriptionStartDate,
                                                   final DateTime bundleStartDate,
                                                   final Plan plan,
                                                   final PhaseType initialPhase,
                                                   final String priceList,
                                                   final DateTime requestedDate)
            throws CatalogApiException, SubscriptionBaseApiException {
        final Catalog catalog = catalogService.getFullCatalog();

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

        final DateTime planStartDate;
        final PlanAlignmentCreate alignment = catalog.planCreateAlignment(planSpecifier, requestedDate);
        switch (alignment) {
            case START_OF_SUBSCRIPTION:
                planStartDate = subscriptionStartDate;
                break;
            case START_OF_BUNDLE:
                planStartDate = bundleStartDate;
                break;
            default:
                throw new SubscriptionBaseError(String.format("Unknown PlanAlignmentCreate %s", alignment));
        }

        return getPhaseAlignments(plan, initialPhase, planStartDate);
    }

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

    private TimedPhase getTimedPhaseOnChange(final DateTime subscriptionStartDate,
                                             final DateTime bundleStartDate,
                                             final PlanPhase currentPhase,
                                             final Plan currentPlan,
                                             final String currentPriceList,
                                             final Plan nextPlan,
                                             final String priceList,
                                             final DateTime requestedDate,
                                             final DateTime effectiveDate,
                                             final WhichPhase which) throws CatalogApiException, SubscriptionBaseApiException {
        final Catalog catalog = catalogService.getFullCatalog();
        final ProductCategory currentCategory = currentPlan.getProduct().getCategory();
        final PlanPhaseSpecifier fromPlanPhaseSpecifier = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
                                                                                 currentCategory,
                                                                                 currentPlan.getBillingPeriod(),
                                                                                 currentPriceList,
                                                                                 currentPhase.getPhaseType());

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

        final DateTime planStartDate;
        final 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 = effectiveDate;
                break;
            case CHANGE_OF_PRICELIST:
                throw new SubscriptionBaseError(String.format("Not implemented yet %s", alignment));
            default:
                throw new SubscriptionBaseError(String.format("Unknown PlanAlignmentChange %s", alignment));
        }

        final List<TimedPhase> timedPhases = getPhaseAlignments(nextPlan, null, planStartDate);
        return getTimedPhase(timedPhases, effectiveDate, which);
    }

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

        final List<TimedPhase> result = new LinkedList<TimedPhase>();
        DateTime curPhaseStart = (initialPhase == null) ? initialPhaseStartDate : null;
        DateTime nextPhaseStart;
        for (final 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) {
                final Duration curPhaseDuration = cur.getDuration();
                nextPhaseStart = addDuration(curPhaseStart, curPhaseDuration);
                if (nextPhaseStart == null) {
                    throw new SubscriptionBaseError(String.format("Unexpected non ending UNLIMITED phase for plan %s",
                                                             plan.getName()));
                }
                curPhaseStart = nextPhaseStart;
            }
        }

        if (initialPhase != null && curPhaseStart == null) {
            throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BAD_PHASE, initialPhase);
        }

        return result;
    }

    // STEPH check for non evergreen Plans and what happens
    private TimedPhase getTimedPhase(final List<TimedPhase> timedPhases, final DateTime effectiveDate, final WhichPhase which) {
        TimedPhase cur = null;
        TimedPhase next = null;
        for (final 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 SubscriptionBaseError(String.format("Unexpected %s TimedPhase", which));
        }
    }



}