org.killbill.billing.subscription.api.svcs.DefaultSubscriptionInternalApi.java Source code

Java tutorial

Introduction

Here is the source code for org.killbill.billing.subscription.api.svcs.DefaultSubscriptionInternalApi.java

Source

/*
 * Copyright 2010-2013 Ning, Inc.
 * Copyright 2014-2017 Groupon, Inc
 * Copyright 2014-2017 The Billing Project, LLC
 *
 * The Billing Project 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.api.svcs;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.annotation.Nullable;

import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.BillingAlignment;
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.Plan;
import org.killbill.billing.catalog.api.PlanChangeResult;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhasePriceOverridesWithCallContext;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.BaseEntitlementWithAddOnsSpecifier;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun.DryRunChangeReason;
import org.killbill.billing.entitlement.api.EntitlementSpecifier;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.subscription.api.SubscriptionApiBase;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOns;
import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionStatusDryRun;
import org.killbill.billing.subscription.api.user.SubscriptionAndAddOnsSpecifier;
import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
import org.killbill.billing.subscription.api.user.SubscriptionSpecifier;
import org.killbill.billing.subscription.engine.addon.AddonUtils;
import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService;
import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.bcd.BCDEvent;
import org.killbill.billing.subscription.events.bcd.BCDEventData;
import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.bcd.BillCycleDayCalculator;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.billing.util.entity.Entity;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
import org.killbill.clock.Clock;
import org.killbill.clock.DefaultClock;
import org.killbill.notificationq.api.NotificationEvent;
import org.killbill.notificationq.api.NotificationEventWithMetadata;
import org.killbill.notificationq.api.NotificationQueue;
import org.killbill.notificationq.api.NotificationQueueService;
import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.inject.Inject;

import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;

public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implements SubscriptionBaseInternalApi {

    private static final Logger log = LoggerFactory.getLogger(DefaultSubscriptionInternalApi.class);

    private final AddonUtils addonUtils;
    private final InternalCallContextFactory internalCallContextFactory;

    private final NotificationQueueService notificationQueueService;

    public static final Comparator<SubscriptionBase> SUBSCRIPTIONS_COMPARATOR = new Comparator<SubscriptionBase>() {

        @Override
        public int compare(final SubscriptionBase o1, final SubscriptionBase o2) {
            if (o1.getCategory() == ProductCategory.BASE) {
                return -1;
            } else if (o2.getCategory() == ProductCategory.BASE) {
                return 1;
            } else {
                return ((DefaultSubscriptionBase) o1).getAlignStartDate()
                        .compareTo(((DefaultSubscriptionBase) o2).getAlignStartDate());
            }
        }
    };

    @Inject
    public DefaultSubscriptionInternalApi(final SubscriptionDao dao, final SubscriptionBaseApiService apiService,
            final NotificationQueueService notificationQueueService, final Clock clock,
            final CatalogService catalogService, final AddonUtils addonUtils,
            final InternalCallContextFactory internalCallContextFactory) {
        super(dao, apiService, clock, catalogService);
        this.addonUtils = addonUtils;
        this.internalCallContextFactory = internalCallContextFactory;
        this.notificationQueueService = notificationQueueService;
    }

    @Override
    public SubscriptionBase createSubscription(final UUID bundleId, final PlanPhaseSpecifier spec,
            final List<PlanPhasePriceOverride> overrides, final DateTime requestedDateWithMs,
            final boolean isMigrated, final InternalCallContext context) throws SubscriptionBaseApiException {
        try {
            final DateTime now = clock.getUTCNow();
            final DateTime effectiveDate = (requestedDateWithMs != null)
                    ? DefaultClock.truncateMs(requestedDateWithMs)
                    : now;
            /*
            if (requestedDate.isAfter(now)) {
            throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_REQUESTED_DATE, now.toString(), requestedDate.toString());
            }
            */

            final CallContext callContext = internalCallContextFactory.createCallContext(context);
            final Catalog catalog = catalogService.getFullCatalog(true, true, context);
            final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(
                    overrides, callContext);

            final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveDate);
            final PlanPhase phase = plan.getAllPhases()[0];
            if (phase == null) {
                throw new SubscriptionBaseError(String.format(
                        "No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
                        spec.getProductName(), spec.getBillingPeriod().toString(), plan.getPriceListName()));
            }

            final SubscriptionBaseBundle bundle = dao.getSubscriptionBundleFromId(bundleId, context);
            if (bundle == null) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BUNDLE, bundleId);
            }

            final DefaultSubscriptionBase baseSubscription = (DefaultSubscriptionBase) dao
                    .getBaseSubscription(bundleId, context);

            // verify the number of subscriptions (of the same kind) allowed per bundle
            if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
                if (plan.getPlansAllowedInBundle() != -1 && plan.getPlansAllowedInBundle() > 0
                        && addonUtils.countExistingAddOnsWithSamePlanName(
                                getSubscriptionsForBundle(bundleId, null, context),
                                plan.getName()) >= plan.getPlansAllowedInBundle()) {
                    // a new ADD_ON subscription of the same plan can't be added because it has reached its limit by bundle
                    throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE,
                            plan.getName());
                }
            }

            final DateTime bundleStartDate = getBundleStartDateWithSanity(bundleId, baseSubscription, plan,
                    effectiveDate, context);
            return apiService.createPlan(new SubscriptionBuilder().setId(UUIDs.randomUUID()).setBundleId(bundleId)
                    .setBundleExternalKey(bundle.getExternalKey()).setCategory(plan.getProduct().getCategory())
                    .setBundleStartDate(bundleStartDate).setAlignStartDate(effectiveDate).setMigrated(isMigrated),
                    plan, spec.getPhaseType(), plan.getPriceListName(), effectiveDate, now, callContext);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    private List<SubscriptionSpecifier> verifyAndBuildSubscriptionSpecifiers(final UUID bundleId,
            final String externalKey, final Iterable<EntitlementSpecifier> entitlements, final boolean isMigrated,
            final InternalCallContext context, final DateTime now, final DateTime effectiveDate,
            final Catalog catalog, final CallContext callContext)
            throws SubscriptionBaseApiException, CatalogApiException {
        final List<SubscriptionSpecifier> subscriptions = new ArrayList<SubscriptionSpecifier>();
        final List<SubscriptionBase> subscriptionsForBundle = getSubscriptionsForBundle(bundleId, null, context);

        for (final EntitlementSpecifier entitlement : entitlements) {

            final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
            if (spec == null) {
                // BP already exists
                continue;
            }

            final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(
                    entitlement.getOverrides(), callContext);

            final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveDate);
            final PlanPhase phase = plan.getAllPhases()[0];
            if (phase == null) {
                throw new SubscriptionBaseError(String.format(
                        "No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
                        spec.getProductName(), spec.getBillingPeriod().toString(), plan.getPriceListName()));
            }

            // verify the number of subscriptions (of the same kind) allowed per bundle and the existing ones
            if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
                if (plan.getPlansAllowedInBundle() != -1 && plan.getPlansAllowedInBundle() > 0) {
                    final int existingAddOnsWithSamePlanName = addonUtils
                            .countExistingAddOnsWithSamePlanName(subscriptionsForBundle, plan.getName());
                    final int currentAddOnsWithSamePlanName = countCurrentAddOnsWithSamePlanName(entitlements,
                            catalog, plan.getName(), effectiveDate, callContext);
                    if ((existingAddOnsWithSamePlanName + currentAddOnsWithSamePlanName) > plan
                            .getPlansAllowedInBundle()) {
                        // a new ADD_ON subscription of the same plan can't be added because it has reached its limit by bundle
                        throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE,
                                plan.getName());
                    }
                }
            }

            final SubscriptionSpecifier subscription = new SubscriptionSpecifier();
            subscription.setRealPriceList(plan.getPriceListName());
            subscription.setEffectiveDate(effectiveDate);
            subscription.setProcessedDate(now);
            subscription.setPlan(plan);
            subscription.setInitialPhase(spec.getPhaseType());
            subscription.setBuilder(new SubscriptionBuilder().setId(UUIDs.randomUUID()).setBundleId(bundleId)
                    .setBundleExternalKey(externalKey).setCategory(plan.getProduct().getCategory())
                    .setBundleStartDate(effectiveDate).setAlignStartDate(effectiveDate).setMigrated(isMigrated));

            subscriptions.add(subscription);
        }
        return subscriptions;
    }

    private boolean sanityAndReorderBPSpecFirst(final Catalog catalog,
            final BaseEntitlementWithAddOnsSpecifier entitlementWithAddOnsSpecifier, final DateTime effectiveDate,
            final List<EntitlementSpecifier> outputEntitlementSpecifier) throws SubscriptionBaseApiException {

        EntitlementSpecifier basePlanSpecifier = null;
        final List<EntitlementSpecifier> addOnSpecifiers = new ArrayList<EntitlementSpecifier>();
        try {
            for (final EntitlementSpecifier cur : entitlementWithAddOnsSpecifier.getEntitlementSpecifier()) {
                final Plan inputPlan = catalog.createOrFindPlan(cur.getPlanPhaseSpecifier(), null, effectiveDate);
                final boolean isBaseSpecifier = inputPlan.getProduct().getCategory() == ProductCategory.BASE;
                if (isBaseSpecifier) {
                    if (basePlanSpecifier == null) {
                        basePlanSpecifier = cur;
                    } else {
                        throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
                    }
                } else {
                    addOnSpecifiers.add(cur);
                }
            }
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }

        if (basePlanSpecifier != null) {
            outputEntitlementSpecifier.add(basePlanSpecifier);
        }
        outputEntitlementSpecifier.addAll(addOnSpecifiers);
        return basePlanSpecifier != null;
    }

    @Override
    public List<SubscriptionBaseWithAddOns> createBaseSubscriptionsWithAddOns(final UUID accountId,
            final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifier,
            final InternalCallContext context) throws SubscriptionBaseApiException {
        try {
            final Catalog catalog = catalogService.getFullCatalog(true, true, context);
            final CallContext callContext = internalCallContextFactory.createCallContext(context);
            final DateTime now = clock.getUTCNow();

            final Collection<SubscriptionAndAddOnsSpecifier> subscriptionAndAddOns = new ArrayList<SubscriptionAndAddOnsSpecifier>();
            for (final BaseEntitlementWithAddOnsSpecifier entitlementWithAddOnsSpecifier : baseEntitlementWithAddOnsSpecifier) {
                final DateTime effectiveDate = (entitlementWithAddOnsSpecifier.getBillingEffectiveDate() != null)
                        ? DefaultClock.truncateMs(
                                entitlementWithAddOnsSpecifier.getBillingEffectiveDate().toDateTimeAtStartOfDay())
                        : now;

                final List<EntitlementSpecifier> reorderedSpecifiers = new ArrayList<EntitlementSpecifier>();
                final boolean isBaseSpecifierExists = sanityAndReorderBPSpecFirst(catalog,
                        entitlementWithAddOnsSpecifier, effectiveDate, reorderedSpecifiers);

                final SubscriptionBaseBundle bundle;
                if (isBaseSpecifierExists) {
                    bundle = createBundleForAccount(accountId, entitlementWithAddOnsSpecifier.getExternalKey(),
                            context);
                } else {
                    final List<SubscriptionBaseBundle> existingBundles = dao
                            .getSubscriptionBundlesForKey(entitlementWithAddOnsSpecifier.getExternalKey(), context);
                    final SubscriptionBaseBundle tmp = getActiveBundleForKeyNotException(existingBundles, dao,
                            clock, context);
                    if (tmp == null) {
                        throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BP,
                                entitlementWithAddOnsSpecifier.getExternalKey());
                    } else if (!tmp.getAccountId().equals(accountId)) {
                        throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS,
                                entitlementWithAddOnsSpecifier.getExternalKey());
                    } else {
                        bundle = tmp;
                    }
                }

                final SubscriptionAndAddOnsSpecifier subscriptionAndAddOnsSpecifier = new SubscriptionAndAddOnsSpecifier(
                        bundle.getId(), effectiveDate,
                        verifyAndBuildSubscriptionSpecifiers(bundle.getId(), bundle.getExternalKey(),
                                reorderedSpecifiers, entitlementWithAddOnsSpecifier.isMigrated(), context, now,
                                effectiveDate, catalog, callContext));
                subscriptionAndAddOns.add(subscriptionAndAddOnsSpecifier);
            }

            return apiService.createPlansWithAddOns(accountId, subscriptionAndAddOns, callContext);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    private int countCurrentAddOnsWithSamePlanName(final Iterable<EntitlementSpecifier> entitlements,
            final Catalog catalog, final String planName, final DateTime effectiveDate,
            final CallContext callContext) throws CatalogApiException {
        int countCurrentAddOns = 0;
        for (final EntitlementSpecifier entitlement : entitlements) {
            final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
            final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(
                    entitlement.getOverrides(), callContext);
            final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveDate);

            if (plan.getName().equalsIgnoreCase(planName) && plan.getProduct().getCategory() != null
                    && ProductCategory.ADD_ON.equals(plan.getProduct().getCategory())) {
                countCurrentAddOns++;
            }
        }
        return countCurrentAddOns;
    }

    @Override
    public void cancelBaseSubscriptions(final Iterable<SubscriptionBase> subscriptions,
            final BillingActionPolicy policy, int accountBillCycleDayLocal, final InternalCallContext context)
            throws SubscriptionBaseApiException {
        apiService.cancelWithPolicyNoValidation(Iterables.<SubscriptionBase, DefaultSubscriptionBase>transform(
                subscriptions, new Function<SubscriptionBase, DefaultSubscriptionBase>() {
                    @Override
                    public DefaultSubscriptionBase apply(final SubscriptionBase subscriptionBase) {
                        try {
                            return getDefaultSubscriptionBase(subscriptionBase, context);
                        } catch (final CatalogApiException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }), policy, accountBillCycleDayLocal, context);
    }

    @Override
    public SubscriptionBaseBundle createBundleForAccount(final UUID accountId, final String bundleKey,
            final InternalCallContext context) throws SubscriptionBaseApiException {

        final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(bundleKey, context);

        //
        // Because the creation of the SubscriptionBundle is not atomic (with creation of Subscription/SubscriptionEvent), we verify if we were left
        // with an empty SubscriptionBaseBundle form a past failing operation (See #684). We only allow reuse if such SubscriptionBaseBundle is fully
        // empty (and don't allow use case where all Subscription are cancelled, which is the condition for that key to be re-used)
        // Such condition should have been checked upstream (to decide whether that key is valid or not)
        //
        final SubscriptionBaseBundle existingBundleForAccount = Iterables
                .tryFind(existingBundles, new Predicate<SubscriptionBaseBundle>() {
                    @Override
                    public boolean apply(final SubscriptionBaseBundle input) {
                        return input.getAccountId().equals(accountId);
                    }
                }).orNull();

        // If Bundle already exists, and there is 0 Subscription, we reuse
        if (existingBundleForAccount != null) {
            try {
                final Map<UUID, List<SubscriptionBase>> accountSubscriptions = dao
                        .getSubscriptionsForAccount(context);
                final List<SubscriptionBase> subscriptions = accountSubscriptions
                        .get(existingBundleForAccount.getId());
                if (subscriptions == null || subscriptions.size() == 0) {
                    return existingBundleForAccount;
                }
            } catch (final CatalogApiException e) {
                throw new SubscriptionBaseApiException(e);
            }
        }

        final DateTime now = clock.getUTCNow();
        final DateTime originalCreatedDate = !existingBundles.isEmpty() ? existingBundles.get(0).getCreatedDate()
                : now;
        final DefaultSubscriptionBaseBundle bundle = new DefaultSubscriptionBaseBundle(bundleKey, accountId, now,
                originalCreatedDate, now, now);

        if (null != bundleKey && bundleKey.length() > 255) {
            throw new SubscriptionBaseApiException(ErrorCode.EXTERNAL_KEY_LIMIT_EXCEEDED);
        }
        return dao.createSubscriptionBundle(bundle, context);
    }

    @Override
    public List<SubscriptionBaseBundle> getBundlesForAccountAndKey(final UUID accountId, final String bundleKey,
            final InternalTenantContext context) throws SubscriptionBaseApiException {
        return dao.getSubscriptionBundlesForAccountAndKey(accountId, bundleKey, context);
    }

    @Override
    public List<SubscriptionBaseBundle> getBundlesForAccount(final UUID accountId,
            final InternalTenantContext context) {
        return dao.getSubscriptionBundleForAccount(accountId, context);
    }

    @Override
    public List<SubscriptionBaseBundle> getBundlesForKey(final String bundleKey,
            final InternalTenantContext context) {
        return dao.getSubscriptionBundlesForKey(bundleKey, context);
    }

    @Override
    public Pagination<SubscriptionBaseBundle> getBundles(final Long offset, final Long limit,
            final InternalTenantContext context) {
        return getEntityPaginationNoException(limit,
                new SourcePaginationBuilder<SubscriptionBundleModelDao, SubscriptionBaseApiException>() {
                    @Override
                    public Pagination<SubscriptionBundleModelDao> build() {
                        return dao.get(offset, limit, context);
                    }
                }, new Function<SubscriptionBundleModelDao, SubscriptionBaseBundle>() {
                    @Override
                    public SubscriptionBaseBundle apply(final SubscriptionBundleModelDao bundleModelDao) {
                        return SubscriptionBundleModelDao.toSubscriptionbundle(bundleModelDao);
                    }
                });
    }

    @Override
    public Pagination<SubscriptionBaseBundle> searchBundles(final String searchKey, final Long offset,
            final Long limit, final InternalTenantContext context) {
        return getEntityPaginationNoException(limit,
                new SourcePaginationBuilder<SubscriptionBundleModelDao, SubscriptionBaseApiException>() {
                    @Override
                    public Pagination<SubscriptionBundleModelDao> build() {
                        return dao.searchSubscriptionBundles(searchKey, offset, limit, context);
                    }
                }, new Function<SubscriptionBundleModelDao, SubscriptionBaseBundle>() {
                    @Override
                    public SubscriptionBaseBundle apply(final SubscriptionBundleModelDao bundleModelDao) {
                        return SubscriptionBundleModelDao.toSubscriptionbundle(bundleModelDao);
                    }
                });

    }

    @Override
    public Iterable<UUID> getNonAOSubscriptionIdsForKey(final String bundleKey,
            final InternalTenantContext context) {
        return dao.getNonAOSubscriptionIdsForKey(bundleKey, context);
    }

    public static SubscriptionBaseBundle getActiveBundleForKeyNotException(
            final Iterable<SubscriptionBaseBundle> existingBundles, final SubscriptionDao dao, final Clock clock,
            final InternalTenantContext context) {
        for (final SubscriptionBaseBundle cur : existingBundles) {
            final List<SubscriptionBase> subscriptions;
            try {
                subscriptions = dao.getSubscriptions(cur.getId(), ImmutableList.<SubscriptionBaseEvent>of(),
                        context);
                for (final SubscriptionBase s : subscriptions) {
                    if (s.getCategory() == ProductCategory.ADD_ON) {
                        continue;
                    }
                    if (s.getEndDate() == null || s.getEndDate().compareTo(clock.getUTCNow()) > 0) {
                        return cur;
                    }
                }
            } catch (final CatalogApiException e) {
                log.warn("Failed to get subscriptions for bundleId='{}'", cur.getId(), e);
                return null;
            }
        }
        return null;
    }

    @Override
    public List<SubscriptionBase> getSubscriptionsForBundle(final UUID bundleId,
            @Nullable final DryRunArguments dryRunArguments, final InternalTenantContext context)
            throws SubscriptionBaseApiException {

        try {
            final List<SubscriptionBaseEvent> outputDryRunEvents = new ArrayList<SubscriptionBaseEvent>();
            final List<SubscriptionBase> outputSubscriptions = new ArrayList<SubscriptionBase>();

            populateDryRunEvents(bundleId, dryRunArguments, outputDryRunEvents, outputSubscriptions, context);
            final List<SubscriptionBase> result;
            result = dao.getSubscriptions(bundleId, outputDryRunEvents, context);
            if (result != null && !result.isEmpty()) {
                outputSubscriptions.addAll(result);
            }
            Collections.sort(outputSubscriptions, DefaultSubscriptionInternalApi.SUBSCRIPTIONS_COMPARATOR);

            return createSubscriptionsForApiUse(outputSubscriptions);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public Map<UUID, List<SubscriptionBase>> getSubscriptionsForAccount(final InternalTenantContext context)
            throws SubscriptionBaseApiException {
        try {
            final Map<UUID, List<SubscriptionBase>> internalSubscriptions = dao.getSubscriptionsForAccount(context);
            final Map<UUID, List<SubscriptionBase>> result = new HashMap<UUID, List<SubscriptionBase>>();
            for (final UUID bundleId : internalSubscriptions.keySet()) {
                result.put(bundleId, createSubscriptionsForApiUse(internalSubscriptions.get(bundleId)));
            }
            return result;
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public SubscriptionBase getBaseSubscription(final UUID bundleId, final InternalTenantContext context)
            throws SubscriptionBaseApiException {
        try {
            final SubscriptionBase result = dao.getBaseSubscription(bundleId, context);
            if (result == null) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId);
            }
            return createSubscriptionForApiUse(result);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public SubscriptionBase getSubscriptionFromId(final UUID id, final InternalTenantContext context)
            throws SubscriptionBaseApiException {
        try {
            final SubscriptionBase result = dao.getSubscriptionFromId(id, context);
            if (result == null) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_SUBSCRIPTION_ID, id);
            }
            return createSubscriptionForApiUse(result);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public SubscriptionBaseBundle getBundleFromId(final UUID id, final InternalTenantContext context)
            throws SubscriptionBaseApiException {
        final SubscriptionBaseBundle result = dao.getSubscriptionBundleFromId(id, context);
        if (result == null) {
            throw new SubscriptionBaseApiException(ErrorCode.SUB_GET_INVALID_BUNDLE_ID, id.toString());
        }
        return result;
    }

    @Override
    public UUID getAccountIdFromSubscriptionId(final UUID subscriptionId, final InternalTenantContext context)
            throws SubscriptionBaseApiException {
        return dao.getAccountIdFromSubscriptionId(subscriptionId, context);
    }

    @Override
    public void setChargedThroughDate(final UUID subscriptionId, final DateTime chargedThruDate,
            final InternalCallContext context) throws SubscriptionBaseApiException {
        try {
            final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) dao
                    .getSubscriptionFromId(subscriptionId, context);
            final SubscriptionBuilder builder = new SubscriptionBuilder(subscription)
                    .setChargedThroughDate(chargedThruDate);

            dao.updateChargedThroughDate(new DefaultSubscriptionBase(builder), context);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public List<EffectiveSubscriptionInternalEvent> getAllTransitions(final SubscriptionBase subscription,
            final InternalTenantContext context) {
        final List<SubscriptionBaseTransition> transitions = subscription.getAllTransitions();
        return convertEffectiveSubscriptionInternalEventFromSubscriptionTransitions(subscription, context,
                transitions);
    }

    @Override
    public List<EffectiveSubscriptionInternalEvent> getBillingTransitions(final SubscriptionBase subscription,
            final InternalTenantContext context) {
        final List<SubscriptionBaseTransition> transitions = ((DefaultSubscriptionBase) subscription)
                .getBillingTransitions();
        return convertEffectiveSubscriptionInternalEventFromSubscriptionTransitions(subscription, context,
                transitions);
    }

    @Override
    public DateTime getDryRunChangePlanEffectiveDate(final SubscriptionBase subscription, final PlanSpecifier spec,
            final DateTime requestedDateWithMs, final BillingActionPolicy requestedPolicy,
            final List<PlanPhasePriceOverride> overrides, final InternalCallContext context)
            throws SubscriptionBaseApiException, CatalogApiException {
        final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);
        final CallContext callContext = internalCallContextFactory.createCallContext(context);

        // verify the number of subscriptions (of the same kind) allowed per bundle
        final Catalog catalog = catalogService.getFullCatalog(true, true, context);
        final DateTime now = clock.getUTCNow();
        final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs)
                : null;
        final DateTime effectiveCatalogDate = effectiveDate != null ? effectiveDate : now;
        final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(
                overrides, callContext);
        final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveCatalogDate);
        if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
            if (plan.getPlansAllowedInBundle() != -1 && plan.getPlansAllowedInBundle() > 0
                    && addonUtils.countExistingAddOnsWithSamePlanName(
                            getSubscriptionsForBundle(subscription.getBundleId(), null, context),
                            plan.getName()) >= plan.getPlansAllowedInBundle()) {
                // the plan can be changed to the new value, because it has reached its limit by bundle
                throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE,
                        plan.getName());
            }
        }
        return apiService.dryRunChangePlan((DefaultSubscriptionBase) subscription, spec, effectiveDate,
                requestedPolicy, tenantContext);
    }

    @Override
    public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(final UUID subscriptionId,
            @Nullable final String baseProductName, final DateTime requestedDate,
            final InternalTenantContext context) throws SubscriptionBaseApiException {
        try {
            final SubscriptionBase subscription = dao.getSubscriptionFromId(subscriptionId, context);
            if (subscription == null) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_SUBSCRIPTION_ID, subscriptionId);
            }
            if (subscription.getCategory() != ProductCategory.BASE) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_DRY_RUN_NOT_BP);
            }

            final List<EntitlementAOStatusDryRun> result = new LinkedList<EntitlementAOStatusDryRun>();

            final List<SubscriptionBase> bundleSubscriptions = dao.getSubscriptions(subscription.getBundleId(),
                    ImmutableList.<SubscriptionBaseEvent>of(), context);
            for (final SubscriptionBase cur : bundleSubscriptions) {
                if (cur.getId().equals(subscriptionId)) {
                    continue;
                }

                // If ADDON is cancelled, skip
                if (cur.getState() == EntitlementState.CANCELLED) {
                    continue;
                }

                final DryRunChangeReason reason;
                // If baseProductName is null, it's a cancellation dry-run. In this case, return all addons, so they are cancelled
                if (baseProductName != null && addonUtils.isAddonIncludedFromProdName(baseProductName,
                        cur.getCurrentPlan(), requestedDate, context)) {
                    reason = DryRunChangeReason.AO_INCLUDED_IN_NEW_PLAN;
                } else if (baseProductName != null && addonUtils.isAddonAvailableFromProdName(baseProductName,
                        cur.getCurrentPlan(), requestedDate, context)) {
                    reason = DryRunChangeReason.AO_AVAILABLE_IN_NEW_PLAN;
                } else {
                    reason = DryRunChangeReason.AO_NOT_AVAILABLE_IN_NEW_PLAN;
                }
                final EntitlementAOStatusDryRun status = new DefaultSubscriptionStatusDryRun(cur.getId(),
                        cur.getCurrentPlan().getProduct().getName(), cur.getCurrentPhase().getPhaseType(),
                        cur.getCurrentPlan().getRecurringBillingPeriod(), cur.getCurrentPriceList().getName(),
                        reason);
                result.add(status);
            }
            return result;
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }

    }

    @Override
    public void updateExternalKey(final UUID bundleId, final String newExternalKey,
            final InternalCallContext context) {
        dao.updateBundleExternalKey(bundleId, newExternalKey, context);
    }

    private void populateDryRunEvents(@Nullable final UUID bundleId,
            @Nullable final DryRunArguments dryRunArguments,
            final Collection<SubscriptionBaseEvent> outputDryRunEvents,
            final Collection<SubscriptionBase> outputSubscriptions, final InternalTenantContext context)
            throws SubscriptionBaseApiException {
        if (dryRunArguments == null || dryRunArguments.getAction() == null) {
            return;
        }

        final DateTime utcNow = clock.getUTCNow();
        List<SubscriptionBaseEvent> dryRunEvents = null;
        try {
            final PlanPhaseSpecifier inputSpec = dryRunArguments.getPlanPhaseSpecifier();
            final boolean isInputSpecNullOrEmpty = inputSpec == null || (inputSpec.getPlanName() == null
                    && inputSpec.getProductName() == null && inputSpec.getBillingPeriod() == null);
            final Catalog catalog = catalogService.getFullCatalog(true, true, context);

            // Create an overridesWithContext with a null context to indicate this is dryRun and no price overriden plan should be created.
            final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(
                    dryRunArguments.getPlanPhasePriceOverrides(), null);
            final Plan plan = isInputSpecNullOrEmpty ? null
                    : catalog.createOrFindPlan(inputSpec, overridesWithContext, utcNow);
            final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);

            switch (dryRunArguments.getAction()) {
            case START_BILLING:

                final DefaultSubscriptionBase baseSubscription = (DefaultSubscriptionBase) dao
                        .getBaseSubscription(bundleId, context);
                final DateTime startEffectiveDate = dryRunArguments.getEffectiveDate() != null
                        ? context.toUTCDateTime(dryRunArguments.getEffectiveDate())
                        : utcNow;
                final DateTime bundleStartDate = getBundleStartDateWithSanity(bundleId, baseSubscription, plan,
                        startEffectiveDate, context);
                final UUID subscriptionId = UUIDs.randomUUID();
                dryRunEvents = apiService.getEventsOnCreation(bundleId, subscriptionId, startEffectiveDate,
                        bundleStartDate, plan, inputSpec.getPhaseType(), plan.getPriceListName(),
                        startEffectiveDate, utcNow, context);
                final SubscriptionBuilder builder = new SubscriptionBuilder().setId(subscriptionId)
                        .setBundleId(bundleId).setBundleExternalKey(null)
                        .setCategory(plan.getProduct().getCategory()).setBundleStartDate(bundleStartDate)
                        .setAlignStartDate(startEffectiveDate);
                final DefaultSubscriptionBase newSubscription = new DefaultSubscriptionBase(builder, apiService,
                        clock);
                newSubscription.rebuildTransitions(dryRunEvents, catalog);
                outputSubscriptions.add(newSubscription);
                break;

            case CHANGE:
                final DefaultSubscriptionBase subscriptionForChange = (DefaultSubscriptionBase) dao
                        .getSubscriptionFromId(dryRunArguments.getSubscriptionId(), context);

                DateTime changeEffectiveDate = getDryRunEffectiveDate(dryRunArguments.getEffectiveDate(),
                        subscriptionForChange, context);
                if (changeEffectiveDate == null) {
                    BillingActionPolicy policy = dryRunArguments.getBillingActionPolicy();
                    if (policy == null) {
                        final PlanChangeResult planChangeResult = apiService
                                .getPlanChangeResult(subscriptionForChange, inputSpec, utcNow, tenantContext);
                        policy = planChangeResult.getPolicy();
                    }
                    // We pass null for billingAlignment, accountTimezone, account BCD because this is not available which means that dryRun with START_OF_TERM BillingPolicy will fail
                    changeEffectiveDate = subscriptionForChange.getPlanChangeEffectiveDate(policy, null, -1,
                            context);
                }
                dryRunEvents = apiService.getEventsOnChangePlan(subscriptionForChange, plan,
                        plan.getPriceListName(), changeEffectiveDate, utcNow, true, context);
                break;

            case STOP_BILLING:
                final DefaultSubscriptionBase subscriptionForCancellation = (DefaultSubscriptionBase) dao
                        .getSubscriptionFromId(dryRunArguments.getSubscriptionId(), context);

                DateTime cancelEffectiveDate = getDryRunEffectiveDate(dryRunArguments.getEffectiveDate(),
                        subscriptionForCancellation, context);
                if (dryRunArguments.getEffectiveDate() == null) {
                    BillingActionPolicy policy = dryRunArguments.getBillingActionPolicy();
                    if (policy == null) {

                        final Plan currentPlan = subscriptionForCancellation.getCurrentPlan();
                        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(currentPlan.getName(),
                                subscriptionForCancellation.getCurrentPhase().getPhaseType());
                        policy = catalogService.getFullCatalog(true, true, context).planCancelPolicy(spec, utcNow);
                    }
                    // We pass null for billingAlignment, accountTimezone, account BCD because this is not available which means that dryRun with START_OF_TERM BillingPolicy will fail
                    cancelEffectiveDate = subscriptionForCancellation.getPlanChangeEffectiveDate(policy, null, -1,
                            context);
                }
                dryRunEvents = apiService.getEventsOnCancelPlan(subscriptionForCancellation, cancelEffectiveDate,
                        utcNow, true, context);
                break;

            default:
                throw new IllegalArgumentException(
                        "Unexpected dryRunArguments action " + dryRunArguments.getAction());
            }
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
        if (dryRunEvents != null && !dryRunEvents.isEmpty()) {
            outputDryRunEvents.addAll(dryRunEvents);
        }
    }

    private DateTime getDryRunEffectiveDate(@Nullable final LocalDate inputDate,
            final DefaultSubscriptionBase subscription, final InternalTenantContext context) {
        if (inputDate == null) {
            return null;
        }

        // We first use context account reference time to get a candidate)
        final DateTime tmp = context.toUTCDateTime(inputDate);
        // If we realize that the candidate is on the same LocalDate boundary as the subscription startDate but a bit prior we correct it to avoid weird things down the line
        if (inputDate.compareTo(context.toLocalDate(subscription.getStartDate())) == 0
                && tmp.compareTo(subscription.getStartDate()) < 0) {
            return subscription.getStartDate();
        } else {
            return tmp;
        }
    }

    @Override
    public Iterable<DateTime> getFutureNotificationsForAccount(final InternalCallContext internalCallContext) {
        try {
            final NotificationQueue notificationQueue = notificationQueueService.getNotificationQueue(
                    DefaultSubscriptionBaseService.SUBSCRIPTION_SERVICE_NAME,
                    DefaultSubscriptionBaseService.NOTIFICATION_QUEUE_NAME);
            final Iterable<NotificationEventWithMetadata<NotificationEvent>> futureNotifications = notificationQueue
                    .getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(),
                            internalCallContext.getTenantRecordId());
            return Iterables.transform(futureNotifications,
                    new Function<NotificationEventWithMetadata<NotificationEvent>, DateTime>() {
                        @Nullable
                        @Override
                        public DateTime apply(final NotificationEventWithMetadata<NotificationEvent> input) {
                            return input.getEffectiveDate();
                        }
                    });
        } catch (final NoSuchNotificationQueue noSuchNotificationQueue) {
            throw new IllegalStateException(noSuchNotificationQueue);
        }
    }

    @Override
    public Map<UUID, DateTime> getNextFutureEventForSubscriptions(final SubscriptionBaseTransitionType eventType,
            final InternalCallContext internalCallContext) {
        final Iterable<SubscriptionBaseEvent> events = dao.getFutureEventsForAccount(internalCallContext);
        final Iterable<SubscriptionBaseEvent> filteredEvents = Iterables.filter(events,
                new Predicate<SubscriptionBaseEvent>() {
                    @Override
                    public boolean apply(final SubscriptionBaseEvent input) {
                        switch (input.getType()) {
                        case PHASE:
                            return eventType == SubscriptionBaseTransitionType.PHASE;
                        case BCD_UPDATE:
                            return eventType == SubscriptionBaseTransitionType.BCD_CHANGE;
                        case API_USER:
                        default:
                            return true;
                        }
                    }
                });
        final Map<UUID, DateTime> result = filteredEvents.iterator().hasNext() ? new HashMap<UUID, DateTime>()
                : ImmutableMap.<UUID, DateTime>of();
        for (final SubscriptionBaseEvent cur : filteredEvents) {
            final DateTime targetDate = result.get(cur.getSubscriptionId());
            if (targetDate == null || targetDate.compareTo(cur.getEffectiveDate()) > 0) {
                result.put(cur.getSubscriptionId(), cur.getEffectiveDate());
            }
        }
        return result;
    }

    @Override
    public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate,
            final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
        final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) getSubscriptionFromId(subscriptionId,
                internalCallContext);
        final DateTime effectiveDate = getEffectiveDateForNewBCD(bcd, effectiveFromDate, internalCallContext);
        final BCDEvent bcdEvent = BCDEventData.createBCDEvent(subscription, effectiveDate, bcd);
        dao.createBCDChangeEvent(subscription, bcdEvent, internalCallContext);
    }

    @Override
    public int getDefaultBillCycleDayLocal(final Map<UUID, Integer> bcdCache, final SubscriptionBase subscription,
            final SubscriptionBase baseSubscription, final PlanPhaseSpecifier planPhaseSpecifier,
            final int accountBillCycleDayLocal, final DateTime effectiveDate, final InternalTenantContext context)
            throws SubscriptionBaseApiException {

        try {
            final Catalog catalog = catalogService.getFullCatalog(true, true, context);
            final BillingAlignment alignment = catalog.billingAlignment(planPhaseSpecifier, effectiveDate);
            return BillCycleDayCalculator.calculateBcdForAlignment(bcdCache, subscription, baseSubscription,
                    alignment, context, accountBillCycleDayLocal);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @VisibleForTesting
    DateTime getEffectiveDateForNewBCD(final int bcd, @Nullable final LocalDate effectiveFromDate,
            final InternalCallContext internalCallContext) {
        if (internalCallContext.getAccountRecordId() == null) {
            throw new IllegalStateException("Need to have a valid context with accountRecordId");
        }

        // Today as seen by this account
        final LocalDate startDate = effectiveFromDate != null ? effectiveFromDate
                : internalCallContext.toLocalDate(clock.getUTCNow());

        // We want to compute a LocalDate in account TZ which maps to the provided 'bcd' and then compute an effectiveDate for when that BCD_CHANGE event needs to be triggered
        //
        // There is a bit of complexity to make sure the date we chose exists (e.g: a BCD of 31 in a february month would not make sense).
        final int currentDay = startDate.getDayOfMonth();
        final int lastDayOfMonth = startDate.dayOfMonth().getMaximumValue();

        final LocalDate requestedDate;
        if (bcd < currentDay) {
            final LocalDate startDatePlusOneMonth = startDate.plusMonths(1);
            final int lastDayOfNextMonth = startDatePlusOneMonth.dayOfMonth().getMaximumValue();
            final int originalBCDORLastDayOfMonth = bcd <= lastDayOfNextMonth ? bcd : lastDayOfNextMonth;
            requestedDate = new LocalDate(startDatePlusOneMonth.getYear(), startDatePlusOneMonth.getMonthOfYear(),
                    originalBCDORLastDayOfMonth);
        } else if (bcd == currentDay && effectiveFromDate == null) {
            // will default to immediate event
            requestedDate = null;
        } else if (bcd <= lastDayOfMonth) {
            requestedDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), bcd);
        } else /* bcd > lastDayOfMonth && bcd > currentDay */ {
            requestedDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), lastDayOfMonth);
        }
        return requestedDate == null ? clock.getUTCNow() : internalCallContext.toUTCDateTime(requestedDate);
    }

    private DateTime getBundleStartDateWithSanity(final UUID bundleId,
            @Nullable final DefaultSubscriptionBase baseSubscription, final Plan plan, final DateTime effectiveDate,
            final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException {
        switch (plan.getProduct().getCategory()) {
        case BASE:
            if (baseSubscription != null && (baseSubscription.getState() == EntitlementState.ACTIVE
                    || baseSubscription.getState() == EntitlementState.PENDING)) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundleId);
            }
            return effectiveDate;

        case ADD_ON:
            if (baseSubscription == null) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BP, bundleId);
            }
            if (effectiveDate.isBefore(baseSubscription.getStartDate())) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_REQUESTED_DATE,
                        effectiveDate.toString(), baseSubscription.getStartDate().toString());
            }
            addonUtils.checkAddonCreationRights(baseSubscription, plan, effectiveDate, context);
            return baseSubscription.getStartDate();

        case STANDALONE:
            if (baseSubscription != null) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundleId);
            }
            // Not really but we don't care, there is no alignment for STANDALONE subscriptions
            return effectiveDate;

        default:
            throw new SubscriptionBaseError(String.format("Can't create subscription of type %s",
                    plan.getProduct().getCategory().toString()));
        }
    }

    private List<EffectiveSubscriptionInternalEvent> convertEffectiveSubscriptionInternalEventFromSubscriptionTransitions(
            final SubscriptionBase subscription, final InternalTenantContext context,
            final Collection<SubscriptionBaseTransition> transitions) {
        return ImmutableList.<EffectiveSubscriptionInternalEvent>copyOf(Collections2.transform(transitions,
                new Function<SubscriptionBaseTransition, EffectiveSubscriptionInternalEvent>() {
                    @Override
                    @Nullable
                    public EffectiveSubscriptionInternalEvent apply(
                            @Nullable final SubscriptionBaseTransition input) {
                        return new DefaultEffectiveSubscriptionEvent((SubscriptionBaseTransitionData) input,
                                ((DefaultSubscriptionBase) subscription).getAlignStartDate(), null,
                                context.getAccountRecordId(), context.getTenantRecordId());
                    }
                }));
    }

    // For forward-compatibility
    private DefaultSubscriptionBase getDefaultSubscriptionBase(final Entity subscriptionBase,
            final InternalTenantContext context) throws CatalogApiException {
        if (subscriptionBase instanceof DefaultSubscriptionBase) {
            return (DefaultSubscriptionBase) subscriptionBase;
        } else {
            // Safe cast, see above
            return (DefaultSubscriptionBase) dao.getSubscriptionFromId(subscriptionBase.getId(), context);
        }
    }
}