
org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseApiService Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2010-2013 Ning, Inc.
* Copyright 2014-2016 Groupon, Inc
* Copyright 2014-2016 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.user;
import java.util.ArrayList;
import java.util.Collection;
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.ReadableInstant;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
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.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogEntity;
import org.killbill.billing.catalog.api.CatalogService;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanChangeResult;
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.Product;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.subscription.alignment.PlanAligner;
import org.killbill.billing.subscription.alignment.TimedPhase;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOns;
import org.killbill.billing.subscription.api.svcs.DefaultPlanPhasePriceOverridesWithCallContext;
import org.killbill.billing.subscription.engine.addon.AddonUtils;
import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.phase.PhaseEvent;
import org.killbill.billing.subscription.events.phase.PhaseEventData;
import org.killbill.billing.subscription.events.user.ApiEventBuilder;
import org.killbill.billing.subscription.events.user.ApiEventCancel;
import org.killbill.billing.subscription.events.user.ApiEventChange;
import org.killbill.billing.subscription.events.user.ApiEventCreate;
import org.killbill.billing.subscription.events.user.ApiEventUncancel;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.clock.Clock;
import org.killbill.clock.DefaultClock;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.inject.Inject;
public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiService {
private final Clock clock;
private final SubscriptionDao dao;
private final CatalogService catalogService;
private final PlanAligner planAligner;
private final AddonUtils addonUtils;
private final InternalCallContextFactory internalCallContextFactory;
@Inject
public DefaultSubscriptionBaseApiService(final Clock clock, final SubscriptionDao dao, final CatalogService catalogService,
final PlanAligner planAligner, final AddonUtils addonUtils,
final InternalCallContextFactory internalCallContextFactory) {
this.clock = clock;
this.catalogService = catalogService;
this.planAligner = planAligner;
this.dao = dao;
this.addonUtils = addonUtils;
this.internalCallContextFactory = internalCallContextFactory;
}
@Override
public DefaultSubscriptionBase createPlan(final SubscriptionBuilder builder, final Plan plan, final PhaseType initialPhase,
final String realPriceList, final DateTime effectiveDate, final DateTime processedDate,
final CallContext context) throws SubscriptionBaseApiException {
final DefaultSubscriptionBase subscription = new DefaultSubscriptionBase(builder, this, clock);
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
try {
final List events = getEventsOnCreation(subscription.getBundleId(), subscription.getId(), subscription.getAlignStartDate(), subscription.getBundleStartDate(),
plan, initialPhase, realPriceList, effectiveDate, processedDate, internalCallContext);
dao.createSubscription(subscription, events, internalCallContext);
subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), catalogService.getFullCatalog(true, true, internalCallContext));
return subscription;
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
}
@Override
public List createPlansWithAddOns(final UUID accountId, final Iterable subscriptionsAndAddOns, final CallContext context) throws SubscriptionBaseApiException {
final Map> eventsMap = new HashMap>();
final Collection> subscriptionBaseAndAddOnsList = new ArrayList>();
final List allSubscriptions = new ArrayList();
for (final SubscriptionAndAddOnsSpecifier subscriptionAndAddOns : subscriptionsAndAddOns) {
final List subscriptionBaseList = new ArrayList();
createEvents(subscriptionAndAddOns.getSubscriptionSpecifiers(), context, eventsMap, subscriptionBaseList);
subscriptionBaseAndAddOnsList.add(subscriptionBaseList);
final SubscriptionBaseWithAddOns subscriptionBaseWithAddOns = new DefaultSubscriptionBaseWithAddOns(subscriptionAndAddOns.getBundleId(),
subscriptionBaseList,
subscriptionAndAddOns.getEffectiveDate());
allSubscriptions.add(subscriptionBaseWithAddOns);
}
final InternalCallContext internalCallContext = createCallContextFromAccountId(accountId, context);
dao.createSubscriptionsWithAddOns(allSubscriptions, eventsMap, internalCallContext);
for (final List subscriptions : subscriptionBaseAndAddOnsList) {
final SubscriptionBase baseSubscription = findBaseSubscription(subscriptions);
rebuildTransitions(internalCallContext, subscriptions, baseSubscription);
}
return allSubscriptions;
}
private void createEvents(final Iterable subscriptions, final CallContext context, final Map> eventsMap, final Collection subscriptionBaseList) throws SubscriptionBaseApiException {
for (final SubscriptionSpecifier subscription : subscriptions) {
try {
final DefaultSubscriptionBase subscriptionBase = new DefaultSubscriptionBase(subscription.getBuilder(), this, clock);
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscriptionBase.getBundleId(), context);
final List events = getEventsOnCreation(subscriptionBase.getBundleId(), subscriptionBase.getId(), subscriptionBase.getAlignStartDate(),
subscriptionBase.getBundleStartDate(), subscription.getPlan(),
subscription.getInitialPhase(), subscription.getRealPriceList(),
subscription.getEffectiveDate(), subscription.getProcessedDate(), internalCallContext);
eventsMap.put(subscriptionBase.getId(), events);
subscriptionBaseList.add(subscriptionBase);
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
}
}
private void rebuildTransitions(final InternalCallContext internalCallContext, final Iterable subscriptions, final SubscriptionBase baseSubscription) throws SubscriptionBaseApiException {
try {
// Safe cast
((DefaultSubscriptionBase) baseSubscription).rebuildTransitions(dao.getEventsForSubscription(baseSubscription.getId(), internalCallContext),
catalogService.getFullCatalog(true, true, internalCallContext));
for (final SubscriptionBase input : subscriptions) {
if (input.getId().equals(baseSubscription.getId())) {
continue;
}
// Safe cast
((DefaultSubscriptionBase) input).rebuildTransitions(dao.getEventsForSubscription(input.getId(), internalCallContext),
catalogService.getFullCatalog(true, true, internalCallContext));
}
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
}
private SubscriptionBase findBaseSubscription(final Iterable subscriptionBaseList) {
return Iterables.tryFind(subscriptionBaseList, new Predicate() {
@Override
public boolean apply(final SubscriptionBase subscription) {
return ProductCategory.BASE.equals(subscription.getCategory());
}
}).orNull();
}
@Override
public boolean cancel(final DefaultSubscriptionBase subscription, final CallContext context) throws SubscriptionBaseApiException {
final EntitlementState currentState = subscription.getState();
if (currentState == EntitlementState.CANCELLED) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_CANCEL_BAD_STATE, subscription.getId(), currentState);
}
final DateTime now = clock.getUTCNow();
final Plan currentPlan = subscription.getCurrentPlan();
final PlanPhaseSpecifier planPhase = new PlanPhaseSpecifier(currentPlan.getName(), null);
try {
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
final BillingActionPolicy policy = catalogService.getFullCatalog(true, true, internalCallContext).planCancelPolicy(planPhase, now);
final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
return doCancelPlan(ImmutableMap.of(subscription, effectiveDate), now, internalCallContext);
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
}
@Override
public boolean cancelWithRequestedDate(final DefaultSubscriptionBase subscription, final DateTime requestedDateWithMs, final CallContext context) throws SubscriptionBaseApiException {
final EntitlementState currentState = subscription.getState();
if (currentState == EntitlementState.CANCELLED) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_CANCEL_BAD_STATE, subscription.getId(), currentState);
}
final DateTime now = clock.getUTCNow();
final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
return doCancelPlan(ImmutableMap.of(subscription, effectiveDate), now, internalCallContext);
}
@Override
public boolean cancelWithPolicy(final DefaultSubscriptionBase subscription, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
final EntitlementState currentState = subscription.getState();
if (currentState == EntitlementState.CANCELLED) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_CANCEL_BAD_STATE, subscription.getId(), currentState);
}
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
return cancelWithPolicyNoValidation(ImmutableList.of(subscription), policy, internalCallContext);
}
@Override
public boolean cancelWithPolicyNoValidation(final Iterable subscriptions, final BillingActionPolicy policy, final InternalCallContext context) throws SubscriptionBaseApiException {
final Map subscriptionsWithEffectiveDate = new HashMap();
final DateTime now = clock.getUTCNow();
for (final DefaultSubscriptionBase subscription : subscriptions) {
final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy);
subscriptionsWithEffectiveDate.put(subscription, effectiveDate);
}
return doCancelPlan(subscriptionsWithEffectiveDate, now, context);
}
private boolean doCancelPlan(final Map subscriptions, final DateTime now, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
final List subscriptionsToBeCancelled = new LinkedList();
final List cancelEvents = new LinkedList();
try {
for (final DefaultSubscriptionBase subscription : subscriptions.keySet()) {
final DateTime effectiveDate = subscriptions.get(subscription);
validateEffectiveDate(subscription, effectiveDate);
subscriptionsToBeCancelled.add(subscription);
cancelEvents.addAll(getEventsOnCancelPlan(subscription, effectiveDate, now, false, internalCallContext));
if (subscription.getCategory() == ProductCategory.BASE) {
subscriptionsToBeCancelled.addAll(computeAddOnsToCancel(cancelEvents, null, subscription.getBundleId(), effectiveDate, internalCallContext));
}
}
dao.cancelSubscriptions(subscriptionsToBeCancelled, cancelEvents, internalCallContext);
boolean allSubscriptionsCancelled = true;
for (final DefaultSubscriptionBase subscription : subscriptions.keySet()) {
final Catalog fullCatalog = catalogService.getFullCatalog(true, true, internalCallContext);
subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), fullCatalog);
allSubscriptionsCancelled = allSubscriptionsCancelled && (subscription.getState() == EntitlementState.CANCELLED);
}
return allSubscriptionsCancelled;
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
}
@Override
public boolean uncancel(final DefaultSubscriptionBase subscription, final CallContext context) throws SubscriptionBaseApiException {
if (!subscription.isSubscriptionFutureCancelled()) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_UNCANCEL_BAD_STATE, subscription.getId().toString());
}
final DateTime now = clock.getUTCNow();
final SubscriptionBaseEvent uncancelEvent = new ApiEventUncancel(new ApiEventBuilder()
.setSubscriptionId(subscription.getId())
.setEffectiveDate(now)
.setFromDisk(true));
final List uncancelEvents = new ArrayList();
uncancelEvents.add(uncancelEvent);
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
final TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription, now, internalCallContext);
final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
PhaseEventData.createNextPhaseEvent(subscription.getId(), nextTimedPhase.getPhase().getName(), nextTimedPhase.getStartPhase()) :
null;
if (nextPhaseEvent != null) {
uncancelEvents.add(nextPhaseEvent);
}
dao.uncancelSubscription(subscription, uncancelEvents, internalCallContext);
try {
final Catalog fullCatalog = catalogService.getFullCatalog(true, true, internalCallContext);
subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), fullCatalog);
return true;
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
}
@Override
public DateTime dryRunChangePlan(final DefaultSubscriptionBase subscription,
final PlanSpecifier spec,
@Nullable final DateTime requestedDateWithMs,
@Nullable final BillingActionPolicy requestedPolicy,
final TenantContext context) throws SubscriptionBaseApiException {
final DateTime now = clock.getUTCNow();
BillingActionPolicy policyMaybeNull = requestedPolicy;
if (requestedDateWithMs == null && requestedPolicy == null) {
final PlanChangeResult planChangeResult = getPlanChangeResult(subscription, spec, now, context);
policyMaybeNull = planChangeResult.getPolicy();
}
if (policyMaybeNull != null) {
return subscription.getPlanChangeEffectiveDate(policyMaybeNull);
} else if (requestedDateWithMs != null) {
return DefaultClock.truncateMs(requestedDateWithMs);
} else {
return now;
}
}
@Override
public DateTime changePlan(final DefaultSubscriptionBase subscription, final PlanSpecifier spec, final List overrides, final CallContext context) throws SubscriptionBaseApiException {
final DateTime now = clock.getUTCNow();
validateEntitlementState(subscription);
final PlanChangeResult planChangeResult = getPlanChangeResult(subscription, spec, now, context);
final DateTime effectiveDate = dryRunChangePlan(subscription, spec, null, planChangeResult.getPolicy(), context);
validateEffectiveDate(subscription, effectiveDate);
try {
doChangePlan(subscription, spec, overrides, effectiveDate, context);
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
return effectiveDate;
}
@Override
public DateTime changePlanWithRequestedDate(final DefaultSubscriptionBase subscription, final PlanSpecifier spec, final List overrides,
final DateTime requestedDateWithMs, final CallContext context) throws SubscriptionBaseApiException {
final DateTime effectiveDate = dryRunChangePlan(subscription, spec, requestedDateWithMs, null, context);
validateEffectiveDate(subscription, effectiveDate);
validateEntitlementState(subscription);
try {
doChangePlan(subscription, spec, overrides, effectiveDate, context);
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
return effectiveDate;
}
@Override
public DateTime changePlanWithPolicy(final DefaultSubscriptionBase subscription, final PlanSpecifier spec, final List overrides, final BillingActionPolicy policy, final CallContext context) throws SubscriptionBaseApiException {
validateEntitlementState(subscription);
final DateTime effectiveDate = dryRunChangePlan(subscription, spec, null, policy, context);
try {
doChangePlan(subscription, spec, overrides, effectiveDate, context);
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
return effectiveDate;
}
@Override
public PlanChangeResult getPlanChangeResult(final DefaultSubscriptionBase subscription, final PlanSpecifier toPlanPhase, final DateTime effectiveDate, final TenantContext context) throws SubscriptionBaseApiException {
final PlanChangeResult planChangeResult;
try {
final InternalTenantContext internalCallContext = createTenantContextFromBundleId(subscription.getBundleId(), context);
final Plan currentPlan = subscription.getCurrentPlan();
final PlanPhaseSpecifier fromPlanPhase = new PlanPhaseSpecifier(currentPlan.getName(),
subscription.getCurrentPhase().getPhaseType());
planChangeResult = catalogService.getFullCatalog(true, true, internalCallContext).planChange(fromPlanPhase, toPlanPhase, effectiveDate);
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
return planChangeResult;
}
private void doChangePlan(final DefaultSubscriptionBase subscription,
final PlanSpecifier spec,
final List overrides,
final DateTime effectiveDate,
final CallContext context) throws SubscriptionBaseApiException, CatalogApiException {
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(overrides, context);
final Plan newPlan = catalogService.getFullCatalog(true, true, internalCallContext).createOrFindPlan(spec, overridesWithContext, effectiveDate, subscription.getStartDate());
if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(newPlan.getProduct().getCategory().toString())) {
if (newPlan.getPlansAllowedInBundle() != -1
&& newPlan.getPlansAllowedInBundle() > 0
&& addonUtils.countExistingAddOnsWithSamePlanName(dao.getSubscriptions(subscription.getBundleId(), null, internalCallContext), newPlan.getName())
>= newPlan.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, newPlan.getName());
}
}
if (newPlan.getProduct().getCategory() != subscription.getCategory()) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_INVALID, subscription.getId());
}
final List addOnSubscriptionsToBeCancelled = new ArrayList();
final List addOnCancelEvents = new ArrayList();
final List changeEvents = getEventsOnChangePlan(subscription, newPlan, newPlan.getPriceListName(), effectiveDate, true, addOnSubscriptionsToBeCancelled, addOnCancelEvents, internalCallContext);
dao.changePlan(subscription, changeEvents, addOnSubscriptionsToBeCancelled, addOnCancelEvents, internalCallContext);
final Catalog fullCatalog = catalogService.getFullCatalog(true, true, internalCallContext);
subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), fullCatalog);
}
@Override
public List getEventsOnCreation(final UUID bundleId, final UUID subscriptionId, final DateTime alignStartDate, final DateTime bundleStartDate,
final Plan plan, final PhaseType initialPhase,
final String realPriceList, final DateTime effectiveDate, final DateTime processedDate,
final InternalTenantContext internalTenantContext) throws CatalogApiException, SubscriptionBaseApiException {
final TimedPhase[] curAndNextPhases = planAligner.getCurrentAndNextTimedPhaseOnCreate(alignStartDate, bundleStartDate, plan, initialPhase,
realPriceList, effectiveDate, internalTenantContext);
final ApiEventBuilder createBuilder = new ApiEventBuilder()
.setSubscriptionId(subscriptionId)
.setEventPlan(plan.getName())
.setEventPlanPhase(curAndNextPhases[0].getPhase().getName())
.setEventPriceList(realPriceList)
.setEffectiveDate(effectiveDate)
.setFromDisk(true);
final SubscriptionBaseEvent creationEvent = new ApiEventCreate(createBuilder);
final TimedPhase nextTimedPhase = curAndNextPhases[1];
final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
PhaseEventData.createNextPhaseEvent(subscriptionId, nextTimedPhase.getPhase().getName(), nextTimedPhase.getStartPhase()) :
null;
final List events = new ArrayList();
events.add(creationEvent);
if (nextPhaseEvent != null) {
events.add(nextPhaseEvent);
}
return events;
}
@Override
public List getEventsOnChangePlan(final DefaultSubscriptionBase subscription, final Plan newPlan,
final String newPriceList, final DateTime effectiveDate, final DateTime processedDate,
final boolean addCancellationAddOnForEventsIfRequired, final InternalTenantContext internalTenantContext) throws CatalogApiException, SubscriptionBaseApiException {
final Collection addOnSubscriptionsToBeCancelled = new ArrayList();
final Collection addOnCancelEvents = new ArrayList();
final List changeEvents = getEventsOnChangePlan(subscription, newPlan, newPriceList, effectiveDate, addCancellationAddOnForEventsIfRequired, addOnSubscriptionsToBeCancelled, addOnCancelEvents, internalTenantContext);
changeEvents.addAll(addOnCancelEvents);
return changeEvents;
}
private List getEventsOnChangePlan(final DefaultSubscriptionBase subscription, final Plan newPlan,
final String newPriceList, final DateTime effectiveDate,
final boolean addCancellationAddOnForEventsIfRequired,
final Collection addOnSubscriptionsToBeCancelled,
final Collection addOnCancelEvents,
final InternalTenantContext internalTenantContext) throws CatalogApiException, SubscriptionBaseApiException {
final TimedPhase currentTimedPhase = planAligner.getCurrentTimedPhaseOnChange(subscription, newPlan, newPriceList, effectiveDate, internalTenantContext);
final SubscriptionBaseEvent changeEvent = new ApiEventChange(new ApiEventBuilder()
.setSubscriptionId(subscription.getId())
.setEventPlan(newPlan.getName())
.setEventPlanPhase(currentTimedPhase.getPhase().getName())
.setEventPriceList(newPriceList)
.setEffectiveDate(effectiveDate)
.setFromDisk(true));
final TimedPhase nextTimedPhase = planAligner.getNextTimedPhaseOnChange(subscription, newPlan, newPriceList, effectiveDate, internalTenantContext);
final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
PhaseEventData.createNextPhaseEvent(subscription.getId(),
nextTimedPhase.getPhase().getName(), nextTimedPhase.getStartPhase()) :
null;
final List changeEvents = new ArrayList();
// Only add the PHASE if it does not coincide with the CHANGE, if not this is 'just' a CHANGE.
changeEvents.add(changeEvent);
if (nextPhaseEvent != null && !nextPhaseEvent.getEffectiveDate().equals(changeEvent.getEffectiveDate())) {
changeEvents.add(nextPhaseEvent);
}
if (subscription.getCategory() == ProductCategory.BASE && addCancellationAddOnForEventsIfRequired) {
final Product currentBaseProduct = changeEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0 ? newPlan.getProduct() : subscription.getCurrentPlan().getProduct();
addOnSubscriptionsToBeCancelled.addAll(addCancellationAddOnForEventsIfRequired(addOnCancelEvents, currentBaseProduct, subscription.getBundleId(), effectiveDate, internalTenantContext));
}
return changeEvents;
}
@Override
public List getEventsOnCancelPlan(final DefaultSubscriptionBase subscription,
final DateTime effectiveDate, final DateTime processedDate,
final boolean addCancellationAddOnForEventsIfRequired, final InternalTenantContext internalTenantContext) throws CatalogApiException {
final List cancelEvents = new ArrayList();
final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
.setSubscriptionId(subscription.getId())
.setEffectiveDate(effectiveDate)
.setFromDisk(true));
cancelEvents.add(cancelEvent);
if (subscription.getCategory() == ProductCategory.BASE && addCancellationAddOnForEventsIfRequired) {
final Product currentBaseProduct = cancelEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0 ? null : subscription.getCurrentPlan().getProduct();
addCancellationAddOnForEventsIfRequired(cancelEvents, currentBaseProduct, subscription.getBundleId(), effectiveDate, internalTenantContext);
}
return cancelEvents;
}
@Override
public int cancelAddOnsIfRequiredOnBasePlanEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent event, final CallContext context) throws CatalogApiException {
final Product baseProduct = (subscription.getState() == EntitlementState.CANCELLED) ? null : subscription.getCurrentPlan().getProduct();
final List cancelEvents = new LinkedList();
final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context);
final List subscriptionsToBeCancelled = computeAddOnsToCancel(cancelEvents, baseProduct, subscription.getBundleId(), event.getEffectiveDate(), internalCallContext);
dao.cancelSubscriptionsOnBasePlanEvent(subscription, event, subscriptionsToBeCancelled, cancelEvents, internalCallContext);
return subscriptionsToBeCancelled.size();
}
private List computeAddOnsToCancel(final Collection cancelEvents, final CatalogEntity baseProduct, final UUID bundleId, final DateTime effectiveDate, final InternalCallContext internalCallContext) throws CatalogApiException {
// If cancellation/change occur in the future, there is nothing to do
final DateTime now = clock.getUTCNow();
if (effectiveDate.compareTo(now) > 0) {
return ImmutableList.of();
} else {
return addCancellationAddOnForEventsIfRequired(cancelEvents, baseProduct, bundleId, effectiveDate, internalCallContext);
}
}
private List addCancellationAddOnForEventsIfRequired(final Collection events, final CatalogEntity baseProduct, final UUID bundleId,
final DateTime effectiveDate, final InternalTenantContext internalTenantContext) throws CatalogApiException {
final List subscriptionsToBeCancelled = new ArrayList();
final List subscriptions = dao.getSubscriptions(bundleId, ImmutableList.of(), internalTenantContext);
for (final SubscriptionBase subscription : subscriptions) {
final DefaultSubscriptionBase cur = (DefaultSubscriptionBase) subscription;
if (cur.getState() == EntitlementState.CANCELLED ||
cur.getCategory() != ProductCategory.ADD_ON) {
continue;
}
final Plan addonCurrentPlan = cur.getCurrentPlan();
if (baseProduct == null ||
addonUtils.isAddonIncludedFromProdName(baseProduct.getName(), addonCurrentPlan, effectiveDate, internalTenantContext) ||
!addonUtils.isAddonAvailableFromProdName(baseProduct.getName(), addonCurrentPlan, effectiveDate, internalTenantContext)) {
//
// Perform AO cancellation using the effectiveDate of the BP
//
final SubscriptionBaseEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
.setSubscriptionId(cur.getId())
.setEffectiveDate(effectiveDate)
.setFromDisk(true));
subscriptionsToBeCancelled.add(cur);
events.add(cancelEvent);
}
}
return subscriptionsToBeCancelled;
}
private void validateEffectiveDate(final SubscriptionBase subscription, final ReadableInstant effectiveDate) throws SubscriptionBaseApiException {
final SubscriptionBaseTransition previousTransition = subscription.getPreviousTransition();
// Our effectiveDate must be after or equal the last transition that already occured (START, PHASE1, PHASE2,...) or the startDate for future started subscription
final DateTime earliestValidDate = previousTransition != null ? previousTransition.getEffectiveTransitionTime() : subscription.getStartDate();
if (effectiveDate.isBefore(earliestValidDate)) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_REQUESTED_DATE,
effectiveDate.toString(), previousTransition != null ? previousTransition.getEffectiveTransitionTime() : "null");
}
}
private void validateEntitlementState(final DefaultSubscriptionBase subscription) throws SubscriptionBaseApiException {
final EntitlementState currentState = subscription.getState();
if (currentState != EntitlementState.ACTIVE) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_NON_ACTIVE, subscription.getId(), currentState);
}
if (subscription.isSubscriptionFutureCancelled()) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_FUTURE_CANCELLED, subscription.getId());
}
}
private InternalCallContext createCallContextFromBundleId(final UUID bundleId, final CallContext context) {
return internalCallContextFactory.createInternalCallContext(bundleId, ObjectType.BUNDLE, context);
}
private InternalCallContext createCallContextFromAccountId(final UUID accountId, final CallContext context) {
return internalCallContextFactory.createInternalCallContext(accountId, ObjectType.ACCOUNT, context);
}
private InternalTenantContext createTenantContextFromBundleId(final UUID bundleId, final TenantContext context) {
return internalCallContextFactory.createInternalTenantContext(bundleId, ObjectType.BUNDLE, context);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy