/*
* 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;
}
});
}
}