killbill-memoizeit
Changes
entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestDefaultBlockingStateDao.java 3(+3 -0)
junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java 4(+4 -0)
subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionApiBase.java 247(+242 -5)
subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionBaseCreateApi.java 347(+347 -0)
Details
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockCatalog.java b/catalog/src/test/java/org/killbill/billing/catalog/MockCatalog.java
index ff6c7e9..9720f64 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/MockCatalog.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockCatalog.java
@@ -142,13 +142,8 @@ public class MockCatalog extends StandaloneCatalog implements Catalog {
}
@Override
- public PriceList findPriceList(final String name, final DateTime requestedDate) throws CatalogApiException {
- return findCurrentPricelist(name);
- }
-
- @Override
public PriceList findPriceListForPlan(final String name, final DateTime requestedDate, final DateTime subscriptionStartDate) throws CatalogApiException {
- return findCurrentPricelist(name);
+ return findCurrentPricelist(findCurrentPlan(name).getPriceListName());
}
@Override
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java b/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java
index 9fde075..e4ed959 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockPlan.java
@@ -96,6 +96,7 @@ public class MockPlan extends DefaultPlan {
setInitialPhases(planPhases);
setPlansAllowedInBundle(plansAllowedInBundle);
setRecurringBillingMode(BillingMode.IN_ADVANCE);
+ setPriceListName(DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
finalPhase.setPlan(this);
for (final DefaultPlanPhase pp : planPhases) {
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestDefaultBlockingStateDao.java b/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestDefaultBlockingStateDao.java
index 1fae808..7c5af2e 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestDefaultBlockingStateDao.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/dao/TestDefaultBlockingStateDao.java
@@ -48,6 +48,9 @@ public class TestDefaultBlockingStateDao extends EntitlementTestSuiteWithEmbedde
@BeforeMethod(groups = "slow")
public void setUp() throws Exception {
+ if (hasFailed()) {
+ return;
+ }
account = createAccount(getAccountData(7));
}
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
index 186af3c..fc918d0 100644
--- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBillingApi.java
@@ -286,7 +286,7 @@ public class TestBillingApi extends JunctionTestSuiteNoDB {
private DateTime createSubscriptionCreationEvent(final Plan nextPlan, final PlanPhase nextPhase) throws CatalogApiException {
final DateTime now = clock.getUTCNow();
final DateTime then = now.minusDays(1);
- final PriceList nextPriceList = catalog.findPriceList(PriceListSet.DEFAULT_PRICELIST_NAME, now);
+ final PriceList nextPriceList = catalog.findPriceListForPlan(nextPlan.getName(), now, now);
final EffectiveSubscriptionInternalEvent t = new MockEffectiveSubscriptionEvent(
eventId, subId, bunId, bunKey, then, now, null, null, null, null, null, EntitlementState.ACTIVE,
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java
index 9436623..fa0af58 100644
--- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java
@@ -77,6 +77,10 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
@BeforeMethod(groups = "fast")
public void beforeMethod() throws Exception {
+ if (hasFailed()) {
+ return;
+ }
+
super.beforeMethod();
account = Mockito.mock(Account.class);
subscription1 = Mockito.mock(SubscriptionBase.class);
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionApiBase.java b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionApiBase.java
index 82cc967..478f24f 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionApiBase.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionApiBase.java
@@ -17,20 +17,47 @@
package org.killbill.billing.subscription.api;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
+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.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
-import org.killbill.billing.catalog.api.CatalogInternalApi;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanChangeResult;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverridesWithCallContext;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.invoice.api.DryRunArguments;
+import org.killbill.billing.subscription.api.svcs.DefaultPlanPhasePriceOverridesWithCallContext;
+import org.killbill.billing.subscription.api.svcs.DefaultSubscriptionInternalApi;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
+import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+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.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.UUIDs;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.clock.Clock;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
public class SubscriptionApiBase {
@@ -44,11 +71,218 @@ public class SubscriptionApiBase {
this.apiService = apiService;
this.clock = clock;
}
- protected List<SubscriptionBase> createSubscriptionsForApiUse(final List<SubscriptionBase> internalSubscriptions) {
+
+ protected SubscriptionBaseBundle getActiveBundleForKey(final String bundleKey, final Catalog catalog, final InternalTenantContext context) throws CatalogApiException {
+ final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(bundleKey, context);
+ for (final SubscriptionBaseBundle cur : existingBundles) {
+ final List<SubscriptionBase> subscriptions;
+ subscriptions = dao.getSubscriptions(cur.getId(), ImmutableList.<SubscriptionBaseEvent>of(), catalog, 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;
+ }
+ }
+ }
+ return null;
+ }
+
+ protected SubscriptionBase getBaseSubscription(final UUID bundleId,
+ final Catalog catalog,
+ final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException {
+ final SubscriptionBase result = dao.getBaseSubscription(bundleId, catalog, context);
+ if (result == null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId);
+ }
+ return createSubscriptionForApiUse(result);
+ }
+
+ protected List<SubscriptionBase> getSubscriptionsForBundle(final UUID bundleId,
+ @Nullable final DryRunArguments dryRunArguments,
+ final Catalog catalog,
+ final AddonUtils addonUtils,
+ final TenantContext tenantContext,
+ final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException {
+
+ final List<SubscriptionBaseEvent> outputDryRunEvents = new ArrayList<SubscriptionBaseEvent>();
+ final List<SubscriptionBase> outputSubscriptions = new ArrayList<SubscriptionBase>();
+
+ populateDryRunEvents(bundleId, dryRunArguments, outputDryRunEvents, outputSubscriptions, catalog, addonUtils, tenantContext, context);
+ final List<SubscriptionBase> result;
+ result = dao.getSubscriptions(bundleId, outputDryRunEvents, catalog, context);
+ if (result != null && !result.isEmpty()) {
+ outputSubscriptions.addAll(result);
+ }
+ Collections.sort(outputSubscriptions, DefaultSubscriptionInternalApi.SUBSCRIPTIONS_COMPARATOR);
+
+ return createSubscriptionsForApiUse(outputSubscriptions);
+ }
+
+ private void populateDryRunEvents(@Nullable final UUID bundleId,
+ @Nullable final DryRunArguments dryRunArguments,
+ final Collection<SubscriptionBaseEvent> outputDryRunEvents,
+ final Collection<SubscriptionBase> outputSubscriptions,
+ final Catalog catalog,
+ final AddonUtils addonUtils,
+ final TenantContext tenantContext,
+ final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException {
+ if (dryRunArguments == null || dryRunArguments.getAction() == null) {
+ return;
+ }
+
+ final DateTime utcNow = clock.getUTCNow();
+ List<SubscriptionBaseEvent> dryRunEvents = null;
+ final PlanPhaseSpecifier inputSpec = dryRunArguments.getPlanPhaseSpecifier();
+ final boolean isInputSpecNullOrEmpty = inputSpec == null ||
+ (inputSpec.getPlanName() == null && inputSpec.getProductName() == null && inputSpec.getBillingPeriod() == null);
+
+ // 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);
+
+ switch (dryRunArguments.getAction()) {
+ case START_BILLING:
+ final SubscriptionBase baseSubscription = dao.getBaseSubscription(bundleId, catalog, context);
+ final DateTime startEffectiveDate = dryRunArguments.getEffectiveDate() != null ? context.toUTCDateTime(dryRunArguments.getEffectiveDate()) : utcNow;
+ final DateTime bundleStartDate = getBundleStartDateWithSanity(bundleId, baseSubscription, plan, startEffectiveDate, catalog, addonUtils, context);
+ final UUID subscriptionId = UUIDs.randomUUID();
+ dryRunEvents = apiService.getEventsOnCreation(subscriptionId, startEffectiveDate, bundleStartDate, plan, inputSpec.getPhaseType(), plan.getPriceListName(),
+ startEffectiveDate, catalog, 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(), catalog, 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, true, catalog, context);
+ break;
+
+ case STOP_BILLING:
+ final DefaultSubscriptionBase subscriptionForCancellation = (DefaultSubscriptionBase) dao.getSubscriptionFromId(dryRunArguments.getSubscriptionId(), catalog, 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 = catalog.planCancelPolicy(spec, subscriptionForCancellation.getStartDate());
+ }
+ // 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, true, catalog, context);
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unexpected dryRunArguments action " + dryRunArguments.getAction());
+ }
+
+ if (dryRunEvents != null && !dryRunEvents.isEmpty()) {
+ outputDryRunEvents.addAll(dryRunEvents);
+ }
+ }
+
+ private DateTime getDryRunEffectiveDate(@Nullable final LocalDate inputDate, final SubscriptionBase 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;
+ }
+ }
+
+ protected DateTime getBundleStartDateWithSanity(final UUID bundleId,
+ @Nullable final SubscriptionBase baseSubscription,
+ final Plan plan,
+ final DateTime effectiveDate,
+ final Catalog catalog,
+ final AddonUtils addonUtils,
+ 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, catalog, 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()));
+ }
+ }
+
+ protected SubscriptionBaseBundle createBundleForAccount(final UUID accountId,
+ final String bundleKey,
+ final boolean renameCancelledBundleIfExist,
+ final Catalog catalog,
+ final CacheController<UUID, UUID> accountIdCacheController,
+ final InternalCallContext context) throws SubscriptionBaseApiException {
+ final DateTime now = context.getCreatedDate();
+ final DefaultSubscriptionBaseBundle bundle = new DefaultSubscriptionBaseBundle(bundleKey, accountId, now, now, now, now);
+ if (null != bundleKey && bundleKey.length() > 255) {
+ throw new SubscriptionBaseApiException(ErrorCode.EXTERNAL_KEY_LIMIT_EXCEEDED);
+ }
+
+ final SubscriptionBaseBundle subscriptionBundle = dao.createSubscriptionBundle(bundle, catalog, renameCancelledBundleIfExist, context);
+ accountIdCacheController.putIfAbsent(bundle.getId(), accountId);
+
+ return subscriptionBundle;
+ }
+
+ protected List<SubscriptionBase> createSubscriptionsForApiUse(final Collection<SubscriptionBase> internalSubscriptions) {
return new ArrayList<SubscriptionBase>(Collections2.transform(internalSubscriptions, new Function<SubscriptionBase, SubscriptionBase>() {
@Override
public SubscriptionBase apply(final SubscriptionBase subscription) {
- return createSubscriptionForApiUse((DefaultSubscriptionBase) subscription);
+ return createSubscriptionForApiUse(subscription);
}
}));
}
@@ -57,9 +291,12 @@ public class SubscriptionApiBase {
return new DefaultSubscriptionBase((DefaultSubscriptionBase) internalSubscription, apiService, clock);
}
- protected DefaultSubscriptionBase createSubscriptionForApiUse(SubscriptionBuilder builder, List<SubscriptionBaseEvent> events, final Catalog catalog, final InternalTenantContext context) throws CatalogApiException {
+ protected DefaultSubscriptionBase createSubscriptionForApiUse(final SubscriptionBuilder builder,
+ final List<SubscriptionBaseEvent> events,
+ final Catalog catalog,
+ final InternalTenantContext context) throws CatalogApiException {
final DefaultSubscriptionBase subscription = new DefaultSubscriptionBase(builder, apiService, clock);
- if (events.size() > 0) {
+ if (!events.isEmpty()) {
subscription.rebuildTransitions(events, catalog);
}
return subscription;
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionBaseCreateApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionBaseCreateApi.java
new file mode 100644
index 0000000..5825a95
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionBaseCreateApi.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverridesWithCallContext;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.EntitlementSpecifier;
+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.SubscriptionBaseWithAddOns;
+import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOnsSpecifier;
+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.SubscriptionBuilder;
+import org.killbill.billing.subscription.api.user.SubscriptionSpecifier;
+import org.killbill.billing.subscription.engine.addon.AddonUtils;
+import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
+import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
+import org.killbill.billing.util.UUIDs;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.clock.Clock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultSubscriptionBaseCreateApi extends SubscriptionApiBase {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultSubscriptionBaseCreateApi.class);
+
+ DefaultSubscriptionBaseCreateApi(final SubscriptionDao dao, final SubscriptionBaseApiService apiService, final Clock clock) {
+ super(dao, apiService, clock);
+ }
+
+ List<SubscriptionBaseWithAddOns> createBaseSubscriptionsWithAddOns(final Iterable<SubscriptionBaseWithAddOnsSpecifier> baseAndAddOnEntitlementsSpecifiers,
+ final boolean renameCancelledBundleIfExist,
+ final Catalog catalog,
+ final AddonUtils addonUtils,
+ final CacheController<UUID, UUID> accountIdCacheController,
+ final CacheController<UUID, UUID> bundleIdCacheController,
+ final CallContext callContext,
+ final InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException {
+ // Prepare the subscription specifiers from the entitlement specifiers
+ final Collection<SubscriptionAndAddOnsSpecifier> baseAndAddOnSubscriptionsSpecifiers = new ArrayList<SubscriptionAndAddOnsSpecifier>();
+ for (final SubscriptionBaseWithAddOnsSpecifier baseAndAddOnEntitlementsSpecifier : baseAndAddOnEntitlementsSpecifiers) {
+ prepareSubscriptionAndAddOnsSpecifier(baseAndAddOnSubscriptionsSpecifiers,
+ baseAndAddOnEntitlementsSpecifier,
+ renameCancelledBundleIfExist,
+ catalog,
+ addonUtils,
+ accountIdCacheController,
+ callContext,
+ context);
+ }
+
+ // Create the subscriptions
+ final List<SubscriptionBaseWithAddOns> subscriptionBaseWithAddOns = apiService.createPlansWithAddOns(callContext.getAccountId(),
+ baseAndAddOnSubscriptionsSpecifiers,
+ catalog,
+ callContext);
+
+ // Populate the caches
+ for (final SubscriptionBaseWithAddOns subscriptionBaseWithAO : subscriptionBaseWithAddOns) {
+ for (final SubscriptionBase subscriptionBase : subscriptionBaseWithAO.getSubscriptionBaseList()) {
+ bundleIdCacheController.putIfAbsent(subscriptionBase.getId(), subscriptionBaseWithAO.getBundle().getId());
+ }
+ }
+
+ return subscriptionBaseWithAddOns;
+ }
+
+ private void prepareSubscriptionAndAddOnsSpecifier(final Collection<SubscriptionAndAddOnsSpecifier> subscriptionAndAddOns,
+ final SubscriptionBaseWithAddOnsSpecifier subscriptionBaseWithAddOnsSpecifier,
+ final boolean renameCancelledBundleIfExist,
+ final Catalog catalog,
+ final AddonUtils addonUtils,
+ final CacheController<UUID, UUID> accountIdCacheController,
+ final CallContext callContext,
+ final InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException {
+ SubscriptionBaseBundle bundle = getBundleWithSanity(subscriptionBaseWithAddOnsSpecifier, catalog, callContext, context);
+
+ final DateTime billingRequestedDateRaw = (subscriptionBaseWithAddOnsSpecifier.getBillingEffectiveDate() != null) ?
+ context.toUTCDateTime(subscriptionBaseWithAddOnsSpecifier.getBillingEffectiveDate()) : context.getCreatedDate();
+
+ final SubscriptionBase baseSubscription;
+ final DateTime billingRequestedDate;
+ if (bundle != null) {
+ baseSubscription = dao.getBaseSubscription(bundle.getId(), catalog, context);
+ billingRequestedDate = computeActualBillingRequestedDate(bundle, billingRequestedDateRaw, baseSubscription, catalog, context);
+ } else {
+ baseSubscription = null;
+ billingRequestedDate = billingRequestedDateRaw;
+ }
+
+ final List<EntitlementSpecifier> reorderedSpecifiers = new ArrayList<EntitlementSpecifier>();
+ final List<Plan> createdOrRetrievedPlans = new ArrayList<Plan>();
+ final boolean hasBaseOrStandalonePlanSpecifier = createPlansIfNeededAndReorderBPOrStandaloneSpecFirstWithSanity(subscriptionBaseWithAddOnsSpecifier,
+ catalog,
+ billingRequestedDate,
+ reorderedSpecifiers,
+ createdOrRetrievedPlans,
+ callContext);
+
+ if (hasBaseOrStandalonePlanSpecifier && baseSubscription != null) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundle.getExternalKey());
+ }
+
+ if (bundle == null && hasBaseOrStandalonePlanSpecifier) {
+ bundle = createBundleForAccount(callContext.getAccountId(),
+ subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey(),
+ renameCancelledBundleIfExist,
+ catalog,
+ accountIdCacheController,
+ context);
+ } else if (bundle == null) {
+ log.warn("Invalid specifier: {}", subscriptionBaseWithAddOnsSpecifier);
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BP, subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey());
+ }
+
+ final List<SubscriptionSpecifier> subscriptionSpecifiers = verifyAndBuildSubscriptionSpecifiers(bundle,
+ hasBaseOrStandalonePlanSpecifier,
+ reorderedSpecifiers,
+ createdOrRetrievedPlans,
+ subscriptionBaseWithAddOnsSpecifier.isMigrated(),
+ billingRequestedDate,
+ catalog,
+ addonUtils,
+ callContext,
+ context);
+ final SubscriptionAndAddOnsSpecifier subscriptionAndAddOnsSpecifier = new SubscriptionAndAddOnsSpecifier(bundle,
+ billingRequestedDate,
+ subscriptionSpecifiers);
+ subscriptionAndAddOns.add(subscriptionAndAddOnsSpecifier);
+ }
+
+ private DateTime computeActualBillingRequestedDate(final SubscriptionBaseBundle bundle,
+ final DateTime billingRequestedDateRaw,
+ @Nullable final SubscriptionBase baseSubscription,
+ final Catalog catalog,
+ final InternalCallContext context) throws CatalogApiException, SubscriptionBaseApiException {
+ DateTime billingRequestedDate = billingRequestedDateRaw;
+ if (baseSubscription != null) {
+ final DateTime baseSubscriptionStartDate = getBaseSubscription(bundle.getId(), catalog, context).getStartDate();
+ billingRequestedDate = billingRequestedDateRaw.isBefore(baseSubscriptionStartDate) ? baseSubscriptionStartDate : billingRequestedDateRaw;
+ }
+ return billingRequestedDate;
+ }
+
+ private SubscriptionBaseBundle getBundleWithSanity(final SubscriptionBaseWithAddOnsSpecifier subscriptionBaseWithAddOnsSpecifier,
+ final Catalog catalog,
+ final TenantContext callContext,
+ final InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException {
+ SubscriptionBaseBundle bundle = null;
+ if (subscriptionBaseWithAddOnsSpecifier.getBundleId() != null) {
+ bundle = dao.getSubscriptionBundleFromId(subscriptionBaseWithAddOnsSpecifier.getBundleId(), context);
+ if (bundle == null ||
+ (subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey() != null && !subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey().equals(bundle.getExternalKey()))) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
+ }
+ } else if (subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey() != null) {
+ final SubscriptionBaseBundle tmp = getActiveBundleForKey(subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey(), catalog, context);
+ if (tmp != null && !tmp.getAccountId().equals(callContext.getAccountId())) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey());
+ } else {
+ bundle = tmp;
+ }
+ }
+ return bundle;
+ }
+
+ private List<SubscriptionSpecifier> verifyAndBuildSubscriptionSpecifiers(final SubscriptionBaseBundle bundle,
+ final boolean hasBaseOrStandalonePlanSpecifier,
+ final List<EntitlementSpecifier> entitlements,
+ final List<Plan> entitlementsPlans,
+ final boolean isMigrated,
+ final DateTime effectiveDate,
+ final Catalog catalog,
+ final AddonUtils addonUtils,
+ final TenantContext callContext,
+ final InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException {
+ final List<SubscriptionSpecifier> subscriptions = new ArrayList<SubscriptionSpecifier>();
+ for (int i = 0; i < entitlements.size(); i++) {
+ final EntitlementSpecifier entitlement = entitlements.get(i);
+ final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
+ if (spec == null) {
+ // BP already exists
+ continue;
+ }
+
+ final Plan plan = entitlementsPlans.get(i);
+ 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) {
+ // TODO We should also look to the specifiers being created for validation
+ final List<SubscriptionBase> subscriptionsForBundle = getSubscriptionsForBundle(bundle.getId(), null, catalog, addonUtils, callContext, context);
+ final int existingAddOnsWithSamePlanName = addonUtils.countExistingAddOnsWithSamePlanName(subscriptionsForBundle, plan.getName());
+ final int currentAddOnsWithSamePlanName = countCurrentAddOnsWithSamePlanName(entitlementsPlans, plan);
+ 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 DateTime bundleStartDate;
+ if (hasBaseOrStandalonePlanSpecifier) {
+ bundleStartDate = effectiveDate;
+ } else {
+ final SubscriptionBase baseSubscription = dao.getBaseSubscription(bundle.getId(), catalog, context);
+ bundleStartDate = getBundleStartDateWithSanity(bundle.getId(), baseSubscription, plan, effectiveDate, catalog, addonUtils, context);
+ }
+
+ final SubscriptionSpecifier subscription = new SubscriptionSpecifier();
+ subscription.setRealPriceList(plan.getPriceListName());
+ subscription.setEffectiveDate(effectiveDate);
+ subscription.setProcessedDate(context.getCreatedDate());
+ subscription.setPlan(plan);
+ subscription.setInitialPhase(spec.getPhaseType());
+ subscription.setBuilder(new SubscriptionBuilder()
+ .setId(UUIDs.randomUUID())
+ .setBundleId(bundle.getId())
+ .setBundleExternalKey(bundle.getExternalKey())
+ .setCategory(plan.getProduct().getCategory())
+ .setBundleStartDate(bundleStartDate)
+ .setAlignStartDate(effectiveDate)
+ .setMigrated(isMigrated));
+
+ subscriptions.add(subscription);
+ }
+
+ return subscriptions;
+ }
+
+ private boolean createPlansIfNeededAndReorderBPOrStandaloneSpecFirstWithSanity(final SubscriptionBaseWithAddOnsSpecifier subscriptionBaseWithAddOnsSpecifier,
+ final Catalog catalog,
+ final DateTime effectiveDate,
+ final Collection<EntitlementSpecifier> outputEntitlementSpecifier,
+ final Collection<Plan> outputEntitlementPlans,
+ final CallContext callContext) throws SubscriptionBaseApiException, CatalogApiException {
+ EntitlementSpecifier basePlanSpecifier = null;
+ Plan basePlan = null;
+ final Collection<EntitlementSpecifier> addOnSpecifiers = new ArrayList<EntitlementSpecifier>();
+ final Collection<EntitlementSpecifier> standaloneSpecifiers = new ArrayList<EntitlementSpecifier>();
+ final Collection<Plan> addOnsPlans = new ArrayList<Plan>();
+ final Collection<Plan> standalonePlans = new ArrayList<Plan>();
+
+ for (final EntitlementSpecifier cur : subscriptionBaseWithAddOnsSpecifier.getEntitlementSpecifiers()) {
+ final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(cur.getOverrides(), callContext);
+ // Called by createBaseSubscriptionsWithAddOns only -- no need for subscription start date
+ final Plan plan = catalog.createOrFindPlan(cur.getPlanPhaseSpecifier(), overridesWithContext, effectiveDate);
+
+ final boolean isBase = isBaseSpecifier(plan);
+ final boolean isStandalone = isStandaloneSpecifier(plan);
+ if (isStandalone) {
+ standaloneSpecifiers.add(cur);
+ standalonePlans.add(plan);
+ } else if (isBase) {
+ if (basePlanSpecifier == null) {
+ basePlanSpecifier = cur;
+ basePlan = plan;
+ } else {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
+ }
+ } else {
+ addOnSpecifiers.add(cur);
+ addOnsPlans.add(plan);
+ }
+ }
+
+ if (basePlanSpecifier != null) {
+ outputEntitlementSpecifier.add(basePlanSpecifier);
+ outputEntitlementPlans.add(basePlan);
+ }
+ outputEntitlementSpecifier.addAll(addOnSpecifiers);
+ outputEntitlementPlans.addAll(addOnsPlans);
+
+ if (!outputEntitlementSpecifier.isEmpty() && !standaloneSpecifiers.isEmpty()) {
+ throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
+ }
+
+ if (standaloneSpecifiers.isEmpty()) {
+ return basePlanSpecifier != null;
+ } else {
+ outputEntitlementSpecifier.addAll(standaloneSpecifiers);
+ outputEntitlementPlans.addAll(standalonePlans);
+ return true;
+ }
+ }
+
+ private boolean isBaseSpecifier(final Plan inputPlan) {
+ return inputPlan.getProduct().getCategory() == ProductCategory.BASE;
+ }
+
+ private boolean isStandaloneSpecifier(final Plan inputPlan) {
+ return inputPlan.getProduct().getCategory() == ProductCategory.STANDALONE;
+ }
+
+ private int countCurrentAddOnsWithSamePlanName(final Iterable<Plan> entitlementsPlans, final Plan currentPlan) {
+ int countCurrentAddOns = 0;
+ for (final Plan plan : entitlementsPlans) {
+ if (plan.getName().equalsIgnoreCase(currentPlan.getName())
+ && plan.getProduct().getCategory() != null
+ && ProductCategory.ADD_ON.equals(plan.getProduct().getCategory())) {
+ countCurrentAddOns++;
+ }
+ }
+ return countCurrentAddOns;
+ }
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index d5c4bb6..d1272e8 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -18,9 +18,7 @@
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;
@@ -42,7 +40,6 @@ import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogInternalApi;
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;
@@ -50,10 +47,8 @@ import org.killbill.billing.catalog.api.ProductCategory;
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;
@@ -63,20 +58,17 @@ import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEv
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.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.cache.AccountIdFromBundleIdCacheLoader;
@@ -93,7 +85,6 @@ 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.NotificationQueueService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -106,7 +97,7 @@ import com.google.inject.Inject;
import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;
-public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implements SubscriptionBaseInternalApi {
+public class DefaultSubscriptionInternalApi extends DefaultSubscriptionBaseCreateApi implements SubscriptionBaseInternalApi {
private static final Logger log = LoggerFactory.getLogger(DefaultSubscriptionInternalApi.class);
@@ -133,7 +124,6 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
@Inject
public DefaultSubscriptionInternalApi(final SubscriptionDao dao,
final SubscriptionBaseApiService apiService,
- final NotificationQueueService notificationQueueService,
final Clock clock,
final CatalogInternalApi catalogInternalApi,
final AddonUtils addonUtils,
@@ -147,230 +137,25 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
this.bundleIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.BUNDLE_ID_FROM_SUBSCRIPTION_ID);
}
- private List<SubscriptionSpecifier> verifyAndBuildSubscriptionSpecifiers(final SubscriptionBaseBundle bundle,
- final boolean hasBaseOrStandalonePlanSpecifier,
- 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>();
- 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) {
- // TODO We should also look to the specifiers being created for validation
- final List<SubscriptionBase> subscriptionsForBundle = getSubscriptionsForBundle(bundle.getId(), null, context);
- 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 DateTime bundleStartDate;
- if (hasBaseOrStandalonePlanSpecifier) {
- bundleStartDate = effectiveDate;
- } else {
- final SubscriptionBase baseSubscription = dao.getBaseSubscription(bundle.getId(), catalog, context);
- bundleStartDate = getBundleStartDateWithSanity(bundle.getId(), baseSubscription, plan, effectiveDate, catalog, context);
- }
-
- 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(bundle.getId())
- .setBundleExternalKey(bundle.getExternalKey())
- .setCategory(plan.getProduct().getCategory())
- .setBundleStartDate(bundleStartDate)
- .setAlignStartDate(effectiveDate)
- .setMigrated(isMigrated));
-
- subscriptions.add(subscription);
- }
- return subscriptions;
- }
-
- private boolean sanityAndReorderBPOrStandaloneSpecFirst(final Catalog catalog,
- final SubscriptionBaseWithAddOnsSpecifier subscriptionBaseWithAddOnsSpecifier,
- final DateTime effectiveDate,
- final Collection<EntitlementSpecifier> outputEntitlementSpecifier) throws SubscriptionBaseApiException {
- EntitlementSpecifier basePlanSpecifier = null;
- final Collection<EntitlementSpecifier> addOnSpecifiers = new ArrayList<EntitlementSpecifier>();
- final Collection<EntitlementSpecifier> standaloneSpecifiers = new ArrayList<EntitlementSpecifier>();
- try {
- for (final EntitlementSpecifier cur : subscriptionBaseWithAddOnsSpecifier.getEntitlementSpecifiers()) {
- final boolean isBase = isBaseSpecifier(catalog, effectiveDate, cur);
- final boolean isStandalone = isStandaloneSpecifier(catalog, effectiveDate, cur);
- if (isStandalone) {
- standaloneSpecifiers.add(cur);
- } else if (isBase) {
- 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);
-
- if (!outputEntitlementSpecifier.isEmpty() && !standaloneSpecifiers.isEmpty()) {
- throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
- }
-
- if (standaloneSpecifiers.isEmpty()) {
- return basePlanSpecifier != null;
- } else {
- outputEntitlementSpecifier.addAll(standaloneSpecifiers);
- return true;
- }
- }
-
- private boolean isBaseSpecifier(final Catalog catalog, final DateTime effectiveDate, final EntitlementSpecifier cur) throws CatalogApiException {
- final Plan inputPlan = catalog.createOrFindPlan(cur.getPlanPhaseSpecifier(), null, effectiveDate);
- return inputPlan.getProduct().getCategory() == ProductCategory.BASE;
- }
-
- private boolean isStandaloneSpecifier(final Catalog catalog, final DateTime effectiveDate, final EntitlementSpecifier cur) throws CatalogApiException {
- final Plan inputPlan = catalog.createOrFindPlan(cur.getPlanPhaseSpecifier(), null, effectiveDate);
- return inputPlan.getProduct().getCategory() == ProductCategory.STANDALONE;
- }
-
@Override
public List<SubscriptionBaseWithAddOns> createBaseSubscriptionsWithAddOns(final Iterable<SubscriptionBaseWithAddOnsSpecifier> subscriptionWithAddOnsSpecifiers, final boolean renameCancelledBundleIfExist, final InternalCallContext context) throws SubscriptionBaseApiException {
try {
final Catalog catalog = catalogInternalApi.getFullCatalog(true, true, context);
final CallContext callContext = internalCallContextFactory.createCallContext(context);
- final UUID accountId = callContext.getAccountId();
-
- final Collection<SubscriptionAndAddOnsSpecifier> subscriptionAndAddOns = new ArrayList<SubscriptionAndAddOnsSpecifier>();
- for (final SubscriptionBaseWithAddOnsSpecifier subscriptionBaseWithAddOnsSpecifier : subscriptionWithAddOnsSpecifiers) {
- final DateTime billingRequestedDateRaw = (subscriptionBaseWithAddOnsSpecifier.getBillingEffectiveDate() != null) ?
- context.toUTCDateTime(subscriptionBaseWithAddOnsSpecifier.getBillingEffectiveDate()) : context.getCreatedDate();
-
- final Collection<EntitlementSpecifier> reorderedSpecifiers = new ArrayList<EntitlementSpecifier>();
- // Note: billingRequestedDateRaw might not be accurate here (add-on with a too early date passed)?
- final boolean hasBaseOrStandalonePlanSpecifier = sanityAndReorderBPOrStandaloneSpecFirst(catalog, subscriptionBaseWithAddOnsSpecifier, billingRequestedDateRaw, reorderedSpecifiers);
-
- DateTime billingRequestedDate = billingRequestedDateRaw;
- SubscriptionBaseBundle bundle = null;
- if (subscriptionBaseWithAddOnsSpecifier.getBundleId() != null) {
- bundle = dao.getSubscriptionBundleFromId(subscriptionBaseWithAddOnsSpecifier.getBundleId(), context);
- if (bundle == null ||
- (subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey() != null && !subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey().equals(bundle.getExternalKey()))) {
- throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
- }
- } else if (subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey() != null &&
- !hasBaseOrStandalonePlanSpecifier) { // Skip the expensive checks if we are about to create the bundle (validation will be done in SubscriptionDao#createSubscriptionBundle)
- final SubscriptionBaseBundle tmp = getActiveBundleForKey(subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey(), catalog, context);
- if (tmp == null) {
- throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BP, subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey());
- } else if (!tmp.getAccountId().equals(accountId)) {
- throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey());
- } else {
- bundle = tmp;
- }
- }
- SubscriptionBase baseSubscription = null;
- if (bundle != null) {
- baseSubscription = dao.getBaseSubscription(bundle.getId(), catalog, context);
- if (baseSubscription != null) {
- final DateTime baseSubscriptionStartDate = getBaseSubscription(bundle.getId(), context).getStartDate();
- billingRequestedDate = billingRequestedDateRaw.isBefore(baseSubscriptionStartDate) ? baseSubscriptionStartDate : billingRequestedDateRaw;
- }
- }
-
- if (bundle == null && hasBaseOrStandalonePlanSpecifier) {
- bundle = createBundleForAccount(accountId,
- subscriptionBaseWithAddOnsSpecifier.getBundleExternalKey(),
- renameCancelledBundleIfExist,
- context);
- } else if (bundle != null && baseSubscription != null && hasBaseOrStandalonePlanSpecifier) {
- throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundle.getExternalKey());
- } else if (bundle == null) {
- log.warn("Invalid specifier: {}", subscriptionBaseWithAddOnsSpecifier);
- throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
- }
-
- final SubscriptionAndAddOnsSpecifier subscriptionAndAddOnsSpecifier = new SubscriptionAndAddOnsSpecifier(bundle,
- billingRequestedDate,
- verifyAndBuildSubscriptionSpecifiers(bundle,
- hasBaseOrStandalonePlanSpecifier,
- reorderedSpecifiers,
- subscriptionBaseWithAddOnsSpecifier.isMigrated(),
- context,
- context.getCreatedDate(),
- billingRequestedDate,
- catalog,
- callContext));
- subscriptionAndAddOns.add(subscriptionAndAddOnsSpecifier);
- }
-
- final List<SubscriptionBaseWithAddOns> subscriptionBaseWithAddOns = apiService.createPlansWithAddOns(accountId, subscriptionAndAddOns, catalog, callContext);
- for (final SubscriptionBaseWithAddOns subscriptionBaseWithAO : subscriptionBaseWithAddOns) {
- for (final SubscriptionBase subscriptionBase : subscriptionBaseWithAO.getSubscriptionBaseList()) {
- bundleIdCacheController.putIfAbsent(subscriptionBase.getId(), subscriptionBaseWithAO.getBundle().getId());
- }
- }
- return subscriptionBaseWithAddOns;
+ return super.createBaseSubscriptionsWithAddOns(subscriptionWithAddOnsSpecifiers,
+ renameCancelledBundleIfExist,
+ catalog,
+ addonUtils,
+ accountIdCacheController,
+ bundleIdCacheController,
+ callContext,
+ context);
} 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 {
@@ -407,9 +192,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
}
try {
final Catalog catalog = catalogInternalApi.getFullCatalog(true, true, context);
- final SubscriptionBaseBundle subscriptionBundle = dao.createSubscriptionBundle(bundle, catalog, renameCancelledBundleIfExist, context);
- accountIdCacheController.putIfAbsent(bundle.getId(), accountId);
- return subscriptionBundle;
+ return super.createBundleForAccount(accountId, bundleKey, renameCancelledBundleIfExist, catalog, accountIdCacheController, context);
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
@@ -475,48 +258,22 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
@Override
public SubscriptionBaseBundle getActiveBundleForKey(final String bundleKey, final Catalog catalog, final InternalTenantContext context) {
- final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(bundleKey, context);
- for (final SubscriptionBaseBundle cur : existingBundles) {
- final List<SubscriptionBase> subscriptions;
- try {
- subscriptions = dao.getSubscriptions(cur.getId(), ImmutableList.<SubscriptionBaseEvent>of(), catalog, 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;
- }
+ try {
+ return super.getActiveBundleForKey(bundleKey, catalog, context);
+ } catch (final CatalogApiException e) {
+ log.warn("Failed to get subscriptions", e);
+ return null;
}
- return null;
}
@Override
public List<SubscriptionBase> getSubscriptionsForBundle(final UUID bundleId,
@Nullable final DryRunArguments dryRunArguments,
final InternalTenantContext context) throws SubscriptionBaseApiException {
-
try {
-
final Catalog catalog = catalogInternalApi.getFullCatalog(true, true, context);
-
- final List<SubscriptionBaseEvent> outputDryRunEvents = new ArrayList<SubscriptionBaseEvent>();
- final List<SubscriptionBase> outputSubscriptions = new ArrayList<SubscriptionBase>();
-
- populateDryRunEvents(bundleId, dryRunArguments, outputDryRunEvents, outputSubscriptions, catalog, context);
- final List<SubscriptionBase> result;
- result = dao.getSubscriptions(bundleId, outputDryRunEvents, catalog, context);
- if (result != null && !result.isEmpty()) {
- outputSubscriptions.addAll(result);
- }
- Collections.sort(outputSubscriptions, DefaultSubscriptionInternalApi.SUBSCRIPTIONS_COMPARATOR);
-
- return createSubscriptionsForApiUse(outputSubscriptions);
+ final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);
+ return super.getSubscriptionsForBundle(bundleId, dryRunArguments, catalog, addonUtils, tenantContext, context);
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
@@ -540,12 +297,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
public SubscriptionBase getBaseSubscription(final UUID bundleId, final InternalTenantContext context) throws SubscriptionBaseApiException {
try {
final Catalog catalog = catalogInternalApi.getFullCatalog(true, true, context);
-
- final SubscriptionBase result = dao.getBaseSubscription(bundleId, catalog, context);
- if (result == null) {
- throw new SubscriptionBaseApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId);
- }
- return createSubscriptionForApiUse(result);
+ return super.getBaseSubscription(bundleId, catalog, context);
} catch (final CatalogApiException e) {
throw new SubscriptionBaseApiException(e);
}
@@ -619,7 +371,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : null;
final DateTime effectiveCatalogDate = effectiveDate != null ? effectiveDate : context.getCreatedDate();
final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(overrides, callContext);
- final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveCatalogDate);
+ final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveCatalogDate, subscription.getStartDate());
if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
if (plan.getPlansAllowedInBundle() != -1
&& plan.getPlansAllowedInBundle() > 0
@@ -687,111 +439,6 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
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 Catalog catalog,
- 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);
-
- // 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, catalog, context);
- final DateTime startEffectiveDate = dryRunArguments.getEffectiveDate() != null ? context.toUTCDateTime(dryRunArguments.getEffectiveDate()) : utcNow;
- final DateTime bundleStartDate = getBundleStartDateWithSanity(bundleId, baseSubscription, plan, startEffectiveDate, catalog, context);
- final UUID subscriptionId = UUIDs.randomUUID();
- dryRunEvents = apiService.getEventsOnCreation(subscriptionId, startEffectiveDate, bundleStartDate, plan, inputSpec.getPhaseType(), plan.getPriceListName(),
- startEffectiveDate, catalog, 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(), catalog, 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, true, catalog, context);
- break;
-
- case STOP_BILLING:
- final DefaultSubscriptionBase subscriptionForCancellation = (DefaultSubscriptionBase) dao.getSubscriptionFromId(dryRunArguments.getSubscriptionId(), catalog, 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 = catalog.planCancelPolicy(spec, subscriptionForCancellation.getStartDate());
- }
- // 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, true, catalog, 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 void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
@@ -903,39 +550,6 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
return requestedDate == null ? internalCallContext.getCreatedDate() : internalCallContext.toUTCDateTime(requestedDate);
}
- private DateTime getBundleStartDateWithSanity(final UUID bundleId, @Nullable final SubscriptionBase baseSubscription, final Plan plan,
- final DateTime effectiveDate, final Catalog catalog, 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, catalog, 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>() {
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
index 4503630..ba21e92 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiAddOn.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
*
- * Ning licenses this file to you under the Apache License, version 2.0
+ * 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:
*