org.killbill.billing.subscription.alignment.PlanAligner.java Source code

Java tutorial

Introduction

Here is the source code for org.killbill.billing.subscription.alignment.PlanAligner.java

Source

/*
 * 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 org.killbill.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 org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogService;
import org.killbill.billing.catalog.api.Duration;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanAlignmentChange;
import org.killbill.billing.catalog.api.PlanAlignmentCreate;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

/**
 * 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 alignStartDate  the subscription (align) startDate for the subscription
     * @param bundleStartDate the bundle startDate used alignment
     * @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 (driven by the catalog policy, i.e. when the creation occurs)
     * @return the current and next phases
     * @throws CatalogApiException         for catalog errors
     * @throws org.killbill.billing.subscription.api.user.SubscriptionBaseApiException for subscription errors
     */
    public TimedPhase[] getCurrentAndNextTimedPhaseOnCreate(final DateTime alignStartDate,
            final DateTime bundleStartDate, final Plan plan, @Nullable final PhaseType initialPhase,
            final String priceList, final DateTime effectiveDate, final InternalTenantContext context)
            throws CatalogApiException, SubscriptionBaseApiException {
        final List<TimedPhase> timedPhases = getTimedPhaseOnCreate(alignStartDate, bundleStartDate, plan,
                initialPhase, effectiveDate, context);
        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 effectiveDate the effective change date (driven by the catalog policy, i.e. when the change occurs)
     * @param newPlanInitialPhaseType the phase on which to start when switching to new plan
     * @return the current phase
     * @throws CatalogApiException         for catalog errors
     * @throws org.killbill.billing.subscription.api.user.SubscriptionBaseApiException for subscription errors
     */
    public TimedPhase getCurrentTimedPhaseOnChange(final DefaultSubscriptionBase subscription, final Plan plan,
            final DateTime effectiveDate, final PhaseType newPlanInitialPhaseType,
            final InternalTenantContext context) throws CatalogApiException, SubscriptionBaseApiException {

        return getTimedPhaseOnChange(subscription, plan, effectiveDate, newPlanInitialPhaseType, WhichPhase.CURRENT,
                context);
    }

    /**
     * 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 effectiveDate the effective change date (driven by the catalog policy, i.e. when the change occurs)
     * @param newPlanInitialPhaseType the phase on which to start when switching to new plan
     * @return the next phase
     * @throws CatalogApiException         for catalog errors
     * @throws org.killbill.billing.subscription.api.user.SubscriptionBaseApiException for subscription errors
     */
    public TimedPhase getNextTimedPhaseOnChange(final DefaultSubscriptionBase subscription, final Plan plan,
            final DateTime effectiveDate, final PhaseType newPlanInitialPhaseType,
            final InternalTenantContext context) throws CatalogApiException, SubscriptionBaseApiException {
        return getTimedPhaseOnChange(subscription, plan, effectiveDate, newPlanInitialPhaseType, WhichPhase.NEXT,
                context);
    }

    /**
     * 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
     * @return the next phase
     */
    public TimedPhase getNextTimedPhase(final DefaultSubscriptionBase subscription, final DateTime effectiveDate,
            final InternalTenantContext context) {

        try {
            final SubscriptionBaseTransition pendingOrLastPlanTransition;
            if (subscription.getState() == EntitlementState.PENDING) {
                pendingOrLastPlanTransition = subscription.getPendingTransition();
            } else {
                pendingOrLastPlanTransition = subscription.getLastTransitionForCurrentPlan();
            }

            switch (pendingOrLastPlanTransition.getTransitionType()) {
            // If we never had any Plan change, borrow the logic for createPlan alignment
            case CREATE:
            case TRANSFER:
                final List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription.getAlignStartDate(),
                        subscription.getBundleStartDate(), pendingOrLastPlanTransition.getNextPlan(),
                        pendingOrLastPlanTransition.getNextPhase().getPhaseType(), effectiveDate, context);
                return getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
            case CHANGE:
                return getTimedPhaseOnChange(subscription.getAlignStartDate(), subscription.getBundleStartDate(),
                        pendingOrLastPlanTransition.getPreviousPhase(),
                        pendingOrLastPlanTransition.getPreviousPlan(), pendingOrLastPlanTransition.getNextPlan(),
                        effectiveDate, pendingOrLastPlanTransition.getEffectiveTransitionTime(),
                        subscription.getAllTransitions().get(0).getNextPhase().getPhaseType(), null,
                        WhichPhase.NEXT, context);
            default:
                throw new SubscriptionBaseError(
                        String.format("Unexpected initial transition %s for current plan %s on subscription %s",
                                pendingOrLastPlanTransition.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, @Nullable final PhaseType initialPhase,
            final DateTime effectiveDate, final InternalTenantContext context)
            throws CatalogApiException, SubscriptionBaseApiException {
        final Catalog catalog = catalogService.getFullCatalog(true, true, context);

        final PlanSpecifier planSpecifier = new PlanSpecifier(plan.getName());

        final DateTime planStartDate;
        final PlanAlignmentCreate alignment = catalog.planCreateAlignment(planSpecifier, effectiveDate);
        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 DateTime effectiveDate, final PhaseType newPlanInitialPhaseType, final WhichPhase which,
            final InternalTenantContext context) throws CatalogApiException, SubscriptionBaseApiException {
        final SubscriptionBaseTransition pendingOrLastPlanTransition;
        if (subscription.getState() == EntitlementState.PENDING) {
            pendingOrLastPlanTransition = subscription.getPendingTransition();
        } else {
            pendingOrLastPlanTransition = subscription.getLastTransitionForCurrentPlan();
        }
        return getTimedPhaseOnChange(subscription.getAlignStartDate(), subscription.getBundleStartDate(),
                pendingOrLastPlanTransition.getNextPhase(), pendingOrLastPlanTransition.getNextPlan(), nextPlan,
                effectiveDate,
                // This method is only called while doing the change, hence we want to pass the change effective date
                effectiveDate, subscription.getAllTransitions().get(0).getNextPhase().getPhaseType(),
                newPlanInitialPhaseType, which, context);
    }

    private TimedPhase getTimedPhaseOnChange(final DateTime subscriptionStartDate, final DateTime bundleStartDate,
            final PlanPhase currentPhase, final Plan currentPlan, final Plan nextPlan, final DateTime effectiveDate,
            final DateTime lastOrCurrentChangeEffectiveDate, final PhaseType originalInitialPhase,
            @Nullable final PhaseType newPlanInitialPhaseType, final WhichPhase which,
            final InternalTenantContext context) throws CatalogApiException, SubscriptionBaseApiException {
        final Catalog catalog = catalogService.getFullCatalog(true, true, context);
        final PlanPhaseSpecifier fromPlanPhaseSpecifier = new PlanPhaseSpecifier(currentPlan.getName(),
                currentPhase.getPhaseType());

        final PlanSpecifier toPlanSpecifier = new PlanSpecifier(nextPlan.getName());
        final PhaseType initialPhase;
        final DateTime planStartDate;
        final PlanAlignmentChange alignment = catalog.planChangeAlignment(fromPlanPhaseSpecifier, toPlanSpecifier,
                effectiveDate);
        switch (alignment) {
        case START_OF_SUBSCRIPTION:
            planStartDate = subscriptionStartDate;
            initialPhase = newPlanInitialPhaseType != null ? newPlanInitialPhaseType
                    : (isPlanContainPhaseType(nextPlan, originalInitialPhase) ? originalInitialPhase : null);
            break;
        case START_OF_BUNDLE:
            planStartDate = bundleStartDate;
            initialPhase = newPlanInitialPhaseType != null ? newPlanInitialPhaseType
                    : (isPlanContainPhaseType(nextPlan, originalInitialPhase) ? originalInitialPhase : null);
            break;
        case CHANGE_OF_PLAN:
            planStartDate = lastOrCurrentChangeEffectiveDate;
            initialPhase = newPlanInitialPhaseType;
            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, initialPhase, 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));
        }
    }

    private boolean isPlanContainPhaseType(final Plan plan, @Nullable final PhaseType phaseType) {
        return Iterables.any(ImmutableList.copyOf(plan.getAllPhases()), new Predicate<PlanPhase>() {
            @Override
            public boolean apply(final PlanPhase input) {
                return input.getPhaseType() == phaseType;
            }
        });
    }
}